现在的 CPU 都提供了单指令流多数据流(single instruction multiple data, SIMD)指令集。最常见的是用于大量的浮点数计算,但其实也可以用在文字处理方面。

其中,SSE4.2 包含了一些专为字符串而设的指令。我们通过使用这些指令,可以大幅提升某些 JSON 解析的性能。

(配图为 2008 年发售的 Intel Core i7 芯片,它采用的 Nehalem 是第一个支持 SSE4.2 的微架构。)

跳过空白字符

我们知道,有一些 JSON 含有缩进(indentation),这些 JSON 有大量的空白字符(whitespace)。在解析 JSON 的时候,需要跳过这些空白字符。这个操作在 RapidJSON 下是这样的(reader.h,为配合版面稍改排版):

template<typename InputStream>
void SkipWhitespace(InputStream& is) {
internal::StreamLocalCopy<InputStream> copy(is);
InputStream& s(copy.s); while (s.Peek() == ' ' ||
s.Peek() == '\n' ||
s.Peek() == '\r' ||
s.Peek() == '\t')
{
s.Take();
}
}

我们先不关注 StreamLocalCopy 等东西。这段代码很简单,就是凡在输入流中遇到4种空白字符,都提取出来跳过,直至流里的字符为非空白字符。

但这种代码会带来很多分支(branching),而且我们每次只能处理一个字符。

SSE4.2

在 Intel 的 SSE4.2 指令集中,有一个 pcmpistrm 指令,它可以一次对一组16个字符与另一组字符作比较,也就是说一个指令可以作最多16×16=256次比较。

对于上面跳过空白字符的需求,我们只需要对16个输入流里的字符与4个空白字符比较,即16×4=64次比较。虽然这样未用尽所有计算能力,但一个指令能代替64个比较以及「或」运算,还是很划算的。

我们可以使用 VC/gcc/clang 都支持的 instrinsic 函数去使用这个指令。这个指令的函数命名为 _mm_cmpistrm(),在nmmintrin.h中定义。

SkipWhitespace 的 SSE4.2 版本只能跳过字符串的输入流,其部分代码如下:

inline const char *SkipWhitespace_SIMD(const char* p) {
// ... 非对齐处理 static const char whitespace[16] = " \n\r\t";
const __m128i w = _mm_load_si128((const __m128i *)&whitespace[0]); for (;; p += 16) {
const __m128i s = _mm_load_si128((const __m128i *)p);
const unsigned r = _mm_cvtsi128_si32(_mm_cmpistrm(w, s,
_SIDD_UBYTE_OPS | _SIDD_CMP_EQUAL_ANY |
_SIDD_BIT_MASK | _SIDD_NEGATIVE_POLARITY)); if (r != 0) { // some of characters is non-whitespace
#ifdef _MSC_VER // Find the index of first non-whitespace
unsigned long offset;
_BitScanForward(&offset, r);
return p + offset;
#else
return p + __builtin_ffs(r) - 1;
#endif
}

解析一下这里 _mm_cmpistrm() 用上了的选项:

  • _SIDD_UBYTE_OPS: 操作单位是无号字节,即16个 unsigned char
  • _SIDD_CMP_EQUAL_ANY: 每次比较 s 里的字符,是否和 w 中的任意字符相等。
  • _SIDD_BIT_MASK: 以比特方式返回结果。
  • _SIDD_NEGATIVE_POLARITY: 把结果反转。这里指返回值的1代表非空白字符。

然后,我们用_mm_cvtsi128_si32()指令,把返回的最低位32字节储存成普通的32位整数。如果含有非空白字符,就使用_BitScanForward()__builtin_ffs()计算出最早出现的非空白字符,并把指针跳到那里返回。

对齐问题

通过 SSE 读写内存,每次可以读写128位(16字节)数据。理想地是使用 128位对齐的地址来读写,这样会最大化读写速度。

最初我使用了 _mm_loadu_si128() 从非对齐的来源字符串读取16个字符。当时我觉得最多就是损失一些时间吧,问题似乎不大。但实际上还是出现了问题

If rapidjson::SkipWhitespace_SIMD(char const*) is called at close to the end of string buffer which has less than 16 bytes of allocated space, the function will read beyond the memory it owns.

In our use case, we parse around 50 million JSON files/buffers per day and

we got hit by the bug around 100 times per day on average before the

workaround.

后来,我估计是因为用非对齐读取,有可能在边界会读到未分配的内存分页,做成很低机率的崩溃。因此,修正方法是先用普通代码处理未对齐的地址,然后才使用 SIMD 进行读取。

inline const char *SkipWhitespace_SIMD(const char* p) {
// ... // 16-byte align to the next boundary
const char* nextAligned = reinterpret_cast<const char*>(
(reinterpret_cast<size_t>(p) + 15) & ~15); while (p != nextAligned)
if (*p == ' ' || *p == '\n' || *p == '\r' || *p == '\t')
++p;
else
return p; // The rest of string using SIMD
// ...
}

快速返回

优化其实还要看实际情况。我们发现,有比较多的情况是,第一个字符已是非空白字符。尤其是已去除空白字符的JSON,上面代码的初始时间还是比较大。因此,我们把第一个字符的检测独立出来。

inline const char *SkipWhitespace_SIMD(const char* p) {
// Fast return for single non-whitespace
if (*p == ' ' || *p == '\n' || *p == '\r' || *p == '\t')
++p;
else
return p; // ...
}

性能测试

测试环境

  • iMac 2.7 GHz Intel Core i5
  • Apple LLVM version 6.1.0 (clang-602.0.49) (based on LLVM 3.6.0svn)

测试用例 1

跳过1M个空白字符1000次。

  • 基本实现: 675 ms
  • SSE4.2: 86 ms
  • strspn: 897 ms

测试用例 2

使用 SAX API 去原位解析(in situ parse)一个含缩进的 671KB sample.json,不处理事件(null handler)。

  • 基本实现: 934 ms
  • SSE4.2: 650 ms

结语

RapidJSON 中使用 SSE4.2 指令集跳过空白字符,可以在一个迭代中进行 64 次字符比较,而且每次读取 128 位数据应该对内存频宽友好。为了兼容更旧的 x86 系 CPU,RapidJSON 也提供了一个 SSE2 的版本,但每个迭代需要执行更多指令,读取可参考源代码

此优化只对含缩进的 JSON 有利,但我们通过「快速返回」使非缩进 JSON 也不会减慢,算是一种权衡之策。在后续的 v1.1 版本中,我希望尝试利用 SIMD 指令去快速扫瞄需处理转义(escaping)的字符,不需转义的部分能使用到 128 位复制至目标缓冲。由于转义符在 JSON 的出现率较低,此举应该能进一步提升整体性能。

最后,关于 x86/x64 系的 SIMD 指令,我推荐 Intel Instrinsic Guide 及 Agner Fog 的5本优化手册

这两期都是比较低阶的东西,下期将会谈一些比较高层一点的,敬请关注。

RapidJSON 代码剖析(二):使用 SSE4.2 优化字符串扫描的更多相关文章

  1. RapidJSON 代码剖析(四):优化 Grisu

    我曾经在知乎的一个答案里谈及到 V8 引擎里实现了 Grisu 算法,我先引用该文的内容简单介绍 Grisu.然后,再谈及 RapidJSON 对它做了的几个底层优化. (配图中的<Grisù& ...

  2. RapidJSON 代码剖析(三):Unicode 的编码与解码

    根据 RFC-7159: 8.1 Character Encoding JSON text SHALL be encoded in UTF-8, UTF-16, or UTF-32. The defa ...

  3. RapidJSON 代码剖析(一):混合任意类型的堆栈

    大家好,这个专栏会分析 RapidJSON (中文使用手册)中一些有趣的 C++ 代码,希望对读者有所裨益. C++ 语法解说 我们先来看一行代码(document.h): bool StartArr ...

  4. Jquery UI 组合树 - ComboTree 集成Wabacus4.1 代码剖析

    Jquery UI 1.3 (组合树 - ComboTree ) 集成Wabacus4.1 集成Spring 代码剖析 使用时,请下载需要Jquery ui包进行配置 combotree.js 的代码 ...

  5. Android4.0图库Gallery2代码分析(二) 数据管理和数据加载

    Android4.0图库Gallery2代码分析(二) 数据管理和数据加载 2012-09-07 11:19 8152人阅读 评论(12) 收藏 举报 代码分析android相册优化工作 Androi ...

  6. HDFS集中式的缓存管理原理与代码剖析--转载

    原文地址:http://yanbohappy.sinaapp.com/?p=468 Hadoop 2.3.0已经发布了,其中最大的亮点就是集中式的缓存管理(HDFS centralized cache ...

  7. HDFS集中式的缓存管理原理与代码剖析

    转载自:http://www.infoq.com/cn/articles/hdfs-centralized-cache/ HDFS集中式的缓存管理原理与代码剖析 Hadoop 2.3.0已经发布了,其 ...

  8. libevent源码深度剖析二

    libevent源码深度剖析二 ——Reactor模式 张亮 前面讲到,整个libevent本身就是一个Reactor,因此本节将专门对Reactor模式进行必要的介绍,并列出libevnet中的几个 ...

  9. java代码解析二维码

    java代码解析二维码一般步骤 本文采用的是google的zxing技术进行解析二维码技术,解析二维码的一般步骤如下: 一.下载zxing-core的jar包: 二.创建一个BufferedImage ...

随机推荐

  1. OC 单例模式

    OC 单例模式 概念 单例模式是一种常用的软件设计模式.在它的核心结构中只包含一个被称为单例的特殊类.通过单例模式可以保证系统中一个类只有一个实例而且该实例易于外界访问,从而方便对实例个数的控制并节约 ...

  2. UITextFeild的用法

    一. 修改占位字符串的 颜色: =======方法一 ====================================== #import "ViewController.h&quo ...

  3. VS的安装

    一 安装过程 我直接在官网下载的 2015版本 ,软件比较大 安装起来比较花时间 同时也装了中文语言包,下面附上安装过程中的一些截图. 二 现在正在摸索如何使用,百度教程,等会附上单元测试.

  4. AEAI Portal V3.5.4升级说明,门户集成平台

    1 总体说明 本次升级是AEAI Portal的一次重要升级,主要扩展了开发社区(论坛).移动门户功能,同时修正一些功能BUG等,具体内容如下: 2 升级内容 功能扩展: 扩展开发社区导航 扩展移动门 ...

  5. Windows Universal 应用 – Tip Calculator

    声明 以下内容取材于 Bob Tabor 的课程<Windows Phone 8.1 Development for Absolute Beginners>,链接地址为:http://ww ...

  6. p2p tcp nat 原理图+源码(原创)

    现今网上p2p的 udp nat穿透 文章 多如牛毛, p2p tcp nat的文章寥寥无几 ,up主研究了几天 终于有所收获,特来向大家分享,请大家多多支持! 1.首先你要有台外网服务器 或者 电信 ...

  7. .NET程序员项目开发必知必会—Dev环境中的集成测试用例执行时上下文环境检查(实战)

    Microsoft.NET 解决方案,项目开发必知必会. 从这篇文章开始我将分享一系列我认为在实际工作中很有必要的一些.NET项目开发的核心技术点,所以我称为必知必会.尽管这一系列是使用.NET/C# ...

  8. 使用SignalR实现消息提醒

    Asp.net SignalR是微软为实现实时通信的一个类库.一般情况下,SignalR会使用JavaScript的长轮询(long polling)的方式来实现客户端和服务器通信,随着Html5中W ...

  9. Shelve Instance 操作详解 - 每天5分钟玩转 OpenStack(38)

    Instance 被 Suspend 后虽然处于 Shut Down 状态,但 Hypervisor 依然在宿主机上为其预留了资源,以便在以后能够成功 Resume. 如果希望释放这些预留资源,可以使 ...

  10. netty3升netty4一失眼成千古恨

    老项目是netty3的,本来想直接改到netty5,但是netty5居然是只支持jdk1.7,很奇怪jdk1.6和jdk1.8都不行..为了兼容jdk1.6加上netty4本来和netty5就差别不大 ...