谈谈 UTF-8 标准和解码的实现

字符集编码的历史

ASCII码

ASCII 码诞生于上世纪 60 年代的美国,它将英文字符和二进制位之间的关系做了统一规定:将 128 个英文的字符映射到一个字节的后 7 位,最前面的一位统一规定为 0。因此 ASCII 码正好使用一个字节存储一个字符,又被称为原始 8 位值,由于最高位始终为 0 ,也被称为 7 位 ASCII 编码。在 ASCII 编码中,将数字 0 映射到 48,将大写字母 A 映射到 65,将小写字母 a 映射到 97 等等。
ASCII 编码简单好用,只占用一个字节,但它只能表示 128 个字符。

非 ASCII 编码

  • 有一些编码会允许使用一个字节的所有 bit 位都用来表示字符,这样一个字节最多就能表示 256 个字符了,比如 Latin-1 编码。
  • 简体中文常见的编码方式是 GB2312,使用两个字节表示一个汉字,所以可以表示 65536(256 x 256)个常用的汉字符号。

Unicode 编码

Unicode 将世界上所有的符号都纳入其中,对世界上所有的符号都赋予一个独一无二的编码,那么,满足了在同一个文本信息中混合使用不同的语言文字的需求。2023 年 9 月 12 日发布的 Unicode 15.1.0 版本已经收录了 149,813 个字符,其中还包含了很多 emoji 符号。每个字符都被映射至一个整数编码,编码范围为 0~0x10FFFF 。注意,这里仅仅用了三个字节而已。

Unicode 编码创建时面临的几个问题:

  • 兼容性的问题:如何才能区别 Unicode 和 ASCII ?计算机怎么知道 3 个字节表示一个符号,而不是分别表示三个符号呢?
  • 存储效率的问题:英文字母只用一个字节表示就够了,如果 Unicode 统一规定,每个符号用 3 或 4 个字节表示,那么每个英文字母前都必然有 2 到 3 个字节是 0,这对于存储来说是极大的浪费,文本文件的大小会因此大出 2~3 倍,这是无法接受的。

Unicode 编码采用了变长存储方式解决了存储效率的问题,采用了特殊标志位解决了兼容性的问题。为此,Unicode 规定了几种储存编码的方式,这些方式被称为 Unicode 转换格式 UTF。经常听到的Unicode 为表现形式,UTF-8 为存储形式。即 UTF-8 解码之后为 Unicode ,Unicode 可以编码成 UTF-8 。同样,存储形式也可以是UTF-32,但是存储的内容解码后依然表现为Unicode。存储形式不唯一,但是内容的表现形式是唯一的。

UTF-8 编码标准

每种 Unicode 转换格式都会把一个编码存储为一到多个编码单元,如 UTF-8 的编码单元为 8 位的字节;UTF-16 的编码单元为 16 位,即 2 个字节;UTF-32 的编码单元为 32 位,即 4 个字节。这里单字节作为一个存储单元,是不存在字节的大端和小段的问题的。但是如果使用 2 字节和 4 字节作为一个存储单元,在存储时会涉及到大小端的问题。大端模式和小端模式的多字节数据在内存中的排列方式有所不同。比如0x0001 在大端模式下被存储为 \x00\x01,而在小端模式下被存储为\x01\x00,与我们的阅读顺序刚好相反 。
所以,为了简单,大家一般使用的是UTF-8 编码标准。
前面提到,Unicode 编码采用了变长存储方式解决了存储效率的问题,采用了特殊标志位解决了兼容性的问题。具体体现如下:

  • 对于单字节符号,字节的第一位设为 0 ,后 7 位为这个符号的 Unicode 码,以兼容 7 位的 ASCII 编码。
  • 对于使用 X 个字节存储的符号,第一个字节的前 X 位设置为 1 ,第 X+1 位设置为 0 。后面字节的前 2 位一律设置为 10 ,剩下的位置依次填充这个符号的 Unicode 码。

当前UTF-8 编码标准一共支持到了4字节的编码,原则上可以支持8字节的,不过万国文字加起来也没有那么多的。编码规则的表格如下,字母 x 表示可用于编码的位,即 Unicode 码分布的位置:

Unicode 码范围(十六进制)UTF-8 编码方式 (二进制)
0x00 ~ 0x7F0xxxxxxx
0x80 ~ 0x7FF110xxxxx 10xxxxxx
0x800 ~ 0xFFFF1110xxxx 10xxxxxx 10xxxxxx
0x10000 ~ 0x10FFFF11110xxx 10xxxxxx 10xxxxxx 10xxxxxx

总结:

  • 如果一个字节的第一位是 0 ,则这个字节单独就是一个ASCII字符
  • 如果第一位是 1 ,则连续有多少个 1 ,就表示当前字符占用多少个字节。所以原则上支持8个字节也就不难理解了。这些都是协议头信息,不是协议数据
  • 后面每个字节的前两位都是10,相当于协议头,不是协议数据
  • 第一个字节的前5位中1的个数(0前面的)表示了该变长编码的实际字节长度(1-4字节),也就是协议长度。

C++解码实现

解码规则

对于 UTF-8 编码的字符,其长度信息并不仅仅由首字节的高 5 位决定。实际上,UTF-8 编码规则中使用了多字节编码来表示较大的 Unicode 字符。

在 UTF-8 编码中,根据首字节的高位,可以确定字符的长度范围:

  • 如果首字节的高位是 0xxxxxxx,则表示该字符是单字节字符,长度为 1。
  • 如果首字节的高位是 110xxxxx,则表示该字符是双字节字符,长度为 2。
  • 如果首字节的高位是 1110xxxx,则表示该字符是三字节字符,长度为 3。
  • 如果首字节的高位是 11110xxx,则表示该字符是四字节字符,长度为 4。
  • 其他首字节的高位模式是无效的,不符合 UTF-8 编码规则。

对于长度超过 4 的字符,UTF-8 编码规则使用了更多的字节来表示。

长度为 5 的字符采用 5 字节编码,首字节的高位是 111110xx
长度为 6 的字符采用 6 字节编码,首字节的高位是 1111110x

然而,需要注意的是,UTF-8 编码规范中规定了 Unicode 字符的范围,而且并不是所有的 Unicode 字符都可以用 UTF-8 编码表示。UTF-8 编码只能表示 Unicode 字符集中的一部分。

在处理 UTF-8 编码时,我们需要根据首字节的高位来确定字符的长度,并根据长度信息来解码后续的字节。对于长度超过 4 的字符,可能需要使用更多的逻辑来处理。这包括检查后续字节的格式和范围,以确保正确解码字符。

加速后的解码规则

我们可以按照上述规则一个一个条件判断去完成解析,但是这会带来较大的性能损失,因为这会让CPU中的分支预测器疲于奔命,每次预测错误都会带来一定的性能损失。所以, Christopher Wellons 想出来了一种a branchless decoder,即无分支的解码器,主要根据前5位组成数字的所有可能性进行查表,完成解码。

再看一下上面的表格,我们来梳理一下首字节的数字规律。

长度通配符最小值最大值
10****00000(0)01111(15)
2110**11000(24)11011(27)
31110*11100(28)11101(29)
41111011110(30)11110(30)

ok,前5位规律如下,其他的数值都是非法的UTF-8 编码,返回长度0就行了。

那么,输入前五位对应的数值,返回长度的映射表如下:

"\1\1\1\1\1\1\1\1\1\1\1\1\1\1\1\1\0\0\0\0\0\0\0\0\2\2\2\2\3\3\4"
11111 5
11111 10
11111 15
10000 20
00002 25
22233 30
4 31

通过这个预定义表,我们可以根据 UTF-8 字符的首字节的高 5 位来快速确定字符的长度。例如,如果首字节的高 5 位是 110**,我们就可以根据预定义表得知对应的字符长度为 2。这种查表的方式非常高效,并且避免了显式的分支判断逻辑,避免了因分支预测带来的cpu流水线排空引发的性能损失。

需要注意的是,这个预定义表是根据 UTF-8 编码规则来设计的,并且假设输入的字符串是合法的 UTF-8 字符串。对于非法的或损坏的 UTF-8 字符串,这个预定义表可能无法正确解析字符的长度。因此,在使用这个预定义表时,需要确保输入的字符串是有效的 UTF-8 编码。

额外吐槽一下,查表法真的是最常见的优化手法了,比如前两天我刚刚看到了这个东西,把数字转换为ASCII码,并且不做任何加法运算。。。。在下目瞪狗呆。

// Converts value in the range [0, 100) to a string.
const char* digits2(size_t value) {// GCC generates slightly better code when value is pointer-size.return &"0001020304050607080910111213141516171819""2021222324252627282930313233343536373839""4041424344454647484950515253545556575859""6061626364656667686970717273747576777879""8081828384858687888990919293949596979899"[value * 2];
}

首字节中的实际数据

我们仍然按照长度1-4来列举所有可能性进行查表,完成解码,这里的通配符和前面协议头的通配符刚刚好相反,首字节一共8位,协议头长度越长,真实数据长度越短。

长度通配符有效长度真实数据的掩码
1X111 111170111 1111 (0x7f)
2XXX1 111150001 1111 (0x1f)
3XXXX 111140000 1111 (0x0f)
4XXXX X11130000 0111 (0x07)

除了首字节,其他字节都是有效长度6位,掩码 0011 1111 (0x3f)

解码数据的拼接

除了首字节实际数据是不定长的,其他字节都是6位,即
|首字节长度| 6 | 6 | 6 |
这里先一次计算完毕4字节的内容,然后根据字节长度再进行右移,扔掉冗余内容。当然,出发点还是为了分支预测的性能。

解码具体实现

//input: raw str s
//output:decoded str c
//output:error flag e
//return:next char* after utf-8 decode 
inline auto utf8_decode(const char* s, uint32_t* c, int* e)-> const char* {constexpr const int masks[] = {0x00, 0x7f, 0x1f, 0x0f, 0x07};constexpr const uint32_t mins[] = {4194304, 0, 128, 2048, 65536};constexpr const int shiftc[] = {0, 18, 12, 6, 0};constexpr const int shifte[] = {0, 6, 4, 2, 0};int len = "\1\1\1\1\1\1\1\1\1\1\1\1\1\1\1\1\0\0\0\0\0\0\0\0\2\2\2\2\3\3\4"[static_cast<unsigned char>(*s) >> 3];// Compute the pointer to the next character early so that the next// iteration can start working on the next character. Neither Clang// nor GCC figure out this reordering on their own.const char* next = s + len + !len;using uchar = unsigned char;// Assume a four-byte character and load four bytes. Unused bits are// shifted out.*c = uint32_t(uchar(s[0]) & masks[len]) << 18;*c |= uint32_t(uchar(s[1]) & 0x3f) << 12;*c |= uint32_t(uchar(s[2]) & 0x3f) << 6;*c |= uint32_t(uchar(s[3]) & 0x3f) << 0;*c >>= shiftc[len];// Accumulate the various error conditions.*e = (*c < mins[len]) << 6;       // non-canonical encoding*e |= ((*c >> 11) == 0x1b) << 7;  // surrogate half?*e |= (*c > 0x10FFFF) << 8;       // out of range?*e |= (uchar(s[1]) & 0xc0) >> 2;*e |= (uchar(s[2]) & 0xc0) >> 4;*e |= uchar(s[3]) >> 6;*e ^= 0x2a;  // top two bits of each tail byte correct?*e >>= shifte[len];return next;
}

codepoint(代码点、码点)

普及一个词——代码点(code point)是指 Unicode 字符集中的字符的唯一标识符。Unicode 使用整数值来表示每个字符,这些整数值被称为代码点。

写作不易,给点个赞吧。。。。。。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/611996.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Vue-13、Vue深度监视

1、监视多级结构中某个属性的变化 <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><title>watch深度监视</title><script type"text/javascript" src"https://cdn.jsdelivr.net/npm…

ROS2——开发第一个节点

ROS2 的包必须在 src 文件夹下&#xff0c;使用下面的命令创建一个包&#xff0c;并设置相关的依赖 ros2 pkg create my_package --dependencies rclcpp std_msgs可以打开包内的 package.xml &#xff0c;查看 depend 有哪些依赖 #include "rclcpp/rclcpp.hpp" int …

一文解决新手所有python环境变量报错问题

问题描述: cmd控制台输入python或pip后会出现下面情况 首先确保安装程序时勾选了安装pip pip的所在目录&#xff0c;可以打开该目录查看是否存在 如果还有问题&#xff0c;确保环境变量配置了python的路径 具体操作步骤 此处的用户环境变量是只针对当前系统用户有效&a…

大气精美网站APP官网HTML源码

源码介绍 大气精美网站APP官网源码&#xff0c;好看实用&#xff0c;记事本修改里面的内容即可&#xff0c;喜欢的朋友可以拿去研究 下载地址 蓝奏云&#xff1a;https://wfr.lanzout.com/itqxN1ko2ovi CSDN免积分下载&#xff1a;https://download.csdn.net/download/huayu…

推荐个超级好用的Vue库,你可能不知道!

高性能 & 简约的 Vue3 Hooks 库 传送门 这是一个等同于阿里出品的 ahooks 的vue3 Hooks函数工具库。 一个高性能且极致简约的 Vue3 Hooks 库&#xff0c;拥有以下丰富特性&#xff1a; &#x1f3c4;&#x1f3fc;‍♂️ 易学易用 &#x1f50b; 支持 SSR &#x1f6f…

【WinForms 窗体】常见的“陷阱”

当涉及到 WinForms 窗体编程时&#xff0c;我们可能会遇到一些常见的问题。在本篇博客中&#xff0c;我将为你提供一些常见问题的解决方案。 跨线程访问控件 在 WinForms 中&#xff0c;当在非UI线程上执行操作并尝试访问 UI 控件时&#xff0c;会引发跨线程访问异常。为了解决…

冬日暖阳行走澳门探寻金沙度假区美食,游行全攻略

澳门金沙度假区是一个集度假、娱乐、购物、美食于一体的综合性旅游胜地。里面光酒店就有7家&#xff08;威尼斯人、巴黎人、四季、瑞吉、康莱德、假日、喜来登&#xff09;&#xff1b;大型购物中心4个&#xff08;四季名店、威尼斯人购物中心、巴黎人购物中心、金沙广场&#…

创建网格(Grid/GridItem)

目录 1、概述 2、布局与约束 3、设置排列方式 3.1设置行列数量与占比 3.2、设置子组件所占行列数 3.3、设置主轴方向 3.4、在网格布局中显示数据 3.5、设置行列间距 4、构建可滚动的网格布局 5、实现简单的日历功能 6、性能优化 1、概述 网格布局是由“行”和“列”分…

福禄电商平台与客服系统:无代码API连接与集成

无代码开发连接电商与客服系统 在电商和客服系统的运营中&#xff0c;技术门槛是非技术型企业面临的挑战之一。解决这个问题的关键是实现不同系统间的高效连接与集成。福禄提供的无代码开发解决方案让企业能够无需API开发即可实现系统之间的无缝连接和集成&#xff0c;帮助企业…

【SpringCloud】@Validated @Valid 不起作用 2.3.0及以上版本

依赖 <!--valid--> <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-validation</artifactId></dependency>检查 版本问题&#xff0c;解决成功

【mysql】报错1349 - View‘s SELECT contains a subquery in the FROM clause

操作 创建视图的sql语句中有不支持子查询 mysql创建视图 select * from (select name,age from table_name where 11 and namea ) tb where 11 and type1问题 报错1349 - View’s SELECT contains a subquery in the FROM clause 原因 原因创建视图的sql语句中有不支持子查…

肺癌文献阅读

第一篇 Five-Year Survival Outcomes With Nivolumab Plus Ipilimumab Versus Chemotherapy as First-Line Treatment for Metastatic Non-Small-Cell Lung Cancer in CheckMate 227 IF:45.3 中科院分区:1区 医学 解析&#xff1a;标题就能很好知道实验结论了&#xff0c;学到…

软件测试|MySQL HAVING分组筛选详解

简介 在 MySQL 数据库中&#xff0c;HAVING 子句用于在使用 GROUP BY 子句对结果进行分组后&#xff0c;对分组后的数据进行筛选和过滤。它允许我们对分组后的结果应用聚合函数&#xff0c;并基于聚合函数的结果进行条件过滤&#xff0c;从而得到我们需要的最终结果集。本文将…

条款21:必须返回对象时,别妄想返回其引用

考虑一个表示有理数的类&#xff0c;其中包含一个计算两个有理数相乘的函数: class Rational { public:Rational(int numerator 0, int denominator 1) :n{ numerator }, d{ denominator }{} private:int n, d; // 分子和分母friend const Rational& operator*(const R…

Fiddler抓包 -- 使用教程

基本按钮 开启/停止&#xff1a;左下角的Capturing按钮 过滤&#xff1a;所有进程、浏览器、非浏览器、全都不抓 怎么抓到你想要的包 ①准备好前置操作 ②清空:通过上方的X按钮&#xff0c;或者按Ctrl X ③操作 ④停止抓包 怎么查看抓到的包的内容 状态码&#xff1a;…

K8S如何扩展副本集

Scaling ReplicaSets 扩展副本集 ReplicaSets are scaled up or down by updating the spec.replicas key on the ReplicaSet object stored in Kubernetes. When a ReplicaSet is scaled up, new Pods are submitted to the Kubernetes API using the Pod template defined o…

Web应用防火墙是什么?聊聊领先WAF解决方案

数字化进程的加速发展&#xff0c;Web站点及各类应用的数量呈现爆发式增长态势。与此同时&#xff0c;利用Web漏洞进行攻击的事件也与日俱增&#xff0c;黑客攻击手段不断升级&#xff0c;包括各种拟人化自动化攻击、API攻击以及0day攻击等&#xff0c;给Web应用安全防护带来了…

通过两台linux主机配置ssh实现互相免密登入

一 1.使用Xshell远程连接工工具生成公钥文件 2.生产密钥参数 3.生成公钥对 4.用户密钥信息 5.公钥注册 二 1.关闭服务端防火墙 ---systemctl stop firewalld 2.检查是否有/root/.ssh目录&#xff0c;没有则创建有则打开/root/.ssh/authorized_keys文件将密钥粘贴创建/ro…

数据结构之Radix和Trie

数据结构可视化演示链接&#xff0c;也就是视频中的网址 Radix树&#xff1a;压缩后的Trie树 Radix叫做基数树&#xff08;压缩树&#xff09;&#xff0c;就是有相同前缀的字符串&#xff0c;其前缀可以作为一个公共的父节点。同时在具体存储上&#xff0c;Radix树的处理是以…

13.Kubernetes应用部署完整流程:从Dockerfile到Ingress发布完整流程

本文以一个简单的Go应用Demo来演示Kubernetes应用部署的完整流程 1、Dockerfile多阶段构建 Dockerfile多阶段构建 [root@docker github]# git clone https://gitee.com/yxydde/http-dump.git [root@docker github]# cd http-dump/ [root@docker http-dump]# cat Dockerfile …