JSON学习笔记-处理空白字符(使用 SSE4.2 优化字符串扫描)

背景:一些JSON含有大量的空白字符(whitspace),在解析JSON时需要跳过这些空白字符。

那么如果我们只是简单的进行处理,当遇在输入流中到这四种空白字符( ,\t,\n,\r,)时,直接跳过,直至流里的字符为非空白字符,虽然很容易理解,但是会带来很多分支且每次只能处理一个字符。

为此,我们采用SSE4.2指令集当中的_mm_cmpistrm()指令对其进行优化。它可以一次对一组16个字符与另一组字符作比较,也就是说一个指令可以作最多16×16=256次比较
(补充:该函数为SIMD的应用,而SIMD是一种并行运算,一次可以执行多条指令,这也是为什么同样是比较而该函数由于逐个比较的原因)

我们用此指令将16个输入流里面的字符与四个空白字符进行比较来替代64次比较及或运算以达到优化的目的。

	for (;; p += 16) {
			const __m128i s = _mm_load_si128((const __m128i*)p);     
		
			const int r = _mm_cmpistrm(w, s,_SIDD_UBYTE_OPS | _SIDD_CMP_EQUAL_ANY | _SIDD_LEAST_SIGNIFICANT | _SIDD_NEGATIVE_POLARITY);
			//如果r!=16,意味着存在非0字符,将指针移到相应位置返回。
			if (r != 16) { 
				return p + r;
			}

<函数解释>:
_mm_cmpistrm(a,b,c): 将a与b进行对比。 c为一个常量,包含以下:指示字符是字节还是单词,要做的比较的类型,以及返回值的格式。
<名词解释>:
_SIDD_UBYTE_OPS: 操作单位是无号字节,即16个 unsigned char。
_SIDD_CMP_EQUAL_ANY: 每次比较 s 里的字符,是否和 w 中的任意字符相等。
**SIDD_LEAST_SIGNIFICANT:**返回设置为1的最右边位的索引
_SIDD_NEGATIVE_POLARITY: 把结果反转。这里指返回值的1代表非空白字符。

即将16字节字符串 s 与空白字符串 w 进行对比,非空白字符为1,空白字符为0。返回最右边位(第一位)非空白字符位置,将指针移动到此位置并返回。

这时我们遇到了两个问题:
(1)字符未对齐,若末尾调用函数而该缓冲区的分配空间小于16字节,则函数将读取其拥有的内存之外的内容,导致崩溃。
---->解决办法: 先用普通代码处理未对齐地址,再进行读取。

const char* nextAligned = reinterpret_cast<const char*>((reinterpret_cast<size_t>(p) + 15)& static_cast<size_t>(~15));

(2)当遇到只需要解析中间代码片段然而全为空白字符时,上述解析代码for循环完毕,p指针并没有移动到末尾。
---->解决办法:
首先我们需要一个使指针移动的函数:

inline const char* SkipWhitespace(const char* p, const char* end) {
		while (p != end && (*p == ' ' || *p == '\n' || *p == '\r' || *p == '\t'))
			++p;
		return p;
	}

再对解析函数进行简单的变化:

inline const char* SkipWhitespace_SIMD(const char* p, const char* end)
{
           
           /......相关操作....../
           
for (; p <= end - 16; p += 16) {
				const __m128i s = _mm_loadu_si128(reinterpret_cast<const __m128i*>(p));
				const int r = _mm_cmpistri(w, s, _SIDD_UBYTE_OPS | _SIDD_CMP_EQUAL_ANY | _SIDD_LEAST_SIGNIFICANT | _SIDD_NEGATIVE_POLARITY);
				//含有非空白字符
				if (r != 16)    
					return p + r;
			}
			//若循环完毕并未返回,则说明全为空白字符,p指针不动,需要调用SkipWhitespace函数移动指针
			return SkipWhitespace(p, end);
}

考虑到存在第一个字符即为非空白字符的情况,在此情况下进行上述解析较为浪费时间,为此需要将第一个字符提取出来进行检测,可以达到快速返回的作用:

inline const char* SkipWhitespace_SIMD(const char* p) {
		// 快速返回(第一个即为非空白字符,)
		if (*p == ' ' || *p == '\n' || *p == '\r' || *p == '\t')
			++p;
		else
			return p;
			/....后续检测操作../
}

以下为完整代码:
(1)整体检测:

inline const char* SkipWhitespace_SIMD(const char* p) {
		// 快速返回(第一个即为非空白字符,)
		if (*p == ' ' || *p == '\n' || *p == '\r' || *p == '\t')
			++p;
		else
			return p;

		// 对齐处理(16字节),c
		const char* nextAligned = reinterpret_cast<const char*>((reinterpret_cast<size_t>(p) + 15)& static_cast<size_t>(~15));
		while (p != nextAligned)
			if (*p == ' ' || *p == '\n' || *p == '\r' || *p == '\t')
				++p;
			else
				return p;

		static const char whitespace[16] = " \n\r\t";
		const __m128i w = _mm_load_si128((const __m128i*) & whitespace[0]);// _mm_load_si128()加载128位值(16字节)

		for (;; p += 16) {
			const __m128i s = _mm_load_si128((const __m128i*)p);     
			const int r = _mm_cmpistrm(w, s,_SIDD_UBYTE_OPS | _SIDD_CMP_EQUAL_ANY | _SIDD_LEAST_SIGNIFICANT | _SIDD_NEGATIVE_POLARITY);
			//如果r!=16,意味着存在非0字符,将指针移到相应位置返回。
			if (r != 16) { 
				return p + r;
			}

(2)部分检测:

inline const char* SkipWhitespace_SIMD(const char* p, const char* end)
		{
			// 快速返回(第一个即为非空白字符,)
			if (*p == ' ' || *p == '\n' || *p == '\r' || *p == '\t')
				++p;
			else
				return p;

			//中间片段处理因为有界所以无需进行对齐处理	
			//处理同上
			static const char whitespace[16] = " \n\r\t";
			const __m128i w = _mm_load_si128((const __m128i*) & whitespace[0]);
			for (; p <= end - 16; p += 16) {
				const __m128i s = _mm_loadu_si128(reinterpret_cast<const __m128i*>(p));
				const int r = _mm_cmpistri(w, s, _SIDD_UBYTE_OPS | _SIDD_CMP_EQUAL_ANY | _SIDD_LEAST_SIGNIFICANT | _SIDD_NEGATIVE_POLARITY);
				//含有非空白字符
				if (r != 16)    
					return p + r;
			}
			//若循环完毕并未返回,则说明全为空白字符,p指针不动,需要调用SkipWhitespace函数移动指针
			return SkipWhitespace(p, end);
		}
		
		inline const char* SkipWhitespace(const char* p, const char* end) {
		while (p != end && (*p == ' ' || *p == '\n' || *p == '\r' || *p == '\t'))
			++p;
		return p;
	}

总结:
我们使用指令_mm_cmpistrm()替代了将一个字符与四个空白字符逐一比对的过程,达到了优化的目的;
考虑到第一个字符即为非空白字符,所以将第一个字符单独进行检测,达到节省时间,快速返回的目的;
检测分为两种情况,整体检测(提供起始指针)和中间部分字符串检测(提供起始指针及结束指针)。整体检测需要将字符对齐后再进行检测,以免造成崩溃,部分检测需要注意全部为空白指针导致起始指针不移动的问题,需要额外调用函数。


<注>
本文是基于Rapidjson中解析JSON的部分代码学习总结,详细代码请参考:RapidJSON: 首页 http://miloyip.github.io/rapidjson/zh-cn/

SSE4.2常用指令介绍:SSE2的一些常用指令集介绍_人工智能_小白_努力-CSDN博客 https://blog.csdn.net/laobai1015/article/details/50886282

_mm_cmpistrm()指令解释以及决定输入字符的类型、要运行的比较以及返回值的格式说明:_mm_cmpestri | Microsoft Docs https://docs.microsoft.com/zh-cn/previous-versions/visualstudio/visual-studio-2010/bb531465(v=vs.100)?redirectedfrom=MSDN

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

想要暴富的小李

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值