分类目录归档:技术探讨

一种更通用的编译期反射

magic_get编译期反射的局限性

magic_get可以实现编译期获取pod类型,是以一种“无痕”的方式实现的,即无需宏、特殊标记、专门工具。看起来确实很精妙,不过也存在一些局限性,比如只能支持pod类型,不能获取反射类型的字段名,也不支持遍历访问对象字段。这些局限性导致magic_get无法在更广泛的环境下应用。

一种更通用的编译期反射方法

基本的反射功能应该包括根据索引获取字段,根据索引获取字段名,遍历对象的所有字段,支持所有类型的对象。
一种更通用的编译期反射应该支持下面这些。

这种编译期反射有几个优点:使用简单,通用,接口完备,支持所有类型的对象而不仅仅是pod类型,非侵入式。

存在的不足之处在于需要定义一个宏,这个非侵入式的宏是用来获取对象的元数据的,是不可少的。magic_get之所以不需要定义宏,是因为利用了pod类型内存连续的特殊性,可以直接转换为内存连续的tuple,这个tuple提供了元数据,不过这个元数据是有缺陷的,即只有字段值而没有其他信息。而定义宏的方式则提供了丰富而完整的元数据信息,会更通用和方便。

由于这种编译期反射方式把对象元数据和元数据的操作分离了,所以用户可以基于这个通用的编译期反射很自由地做自己感兴趣的事情。

编译期反射可以用来做什么

编译期反射非常适合用来做ORM引擎和序列化/反序列化引擎,以ORM为例,我们可以基于反射来做多种数据库的ORM,比如sqlite, mysql, postgresql,oracle,sqlserver等数据库。有了ORM之后使用起来会非常方便,ORM给用户提供简单通用的接口,把数据库差异、对象和实体相互转换等繁琐的细节都屏蔽了。以基于编译期反射实现sqlite的ORM为例:

从上面的例子可以看到,基于编译期反射实现的ORM接口非常简单、通用和强大,你几乎可以用这几个接口做任何事。即使对于其他数据库,接口仍然保持不变,这将会极大地提高数据库开发效率和降低数据库开发的难度,这就是编译期反射的威力!

关于编译期反射的实现原理和ORM的实现原理敬请关注即将开始的purecpp社区第一期技术公开课

如何优雅地管理constant buffer

1. Preface

Constant buffer是我们在编写shader的时候,打交道最多的一种buffer resource了。constant表明了constant buffer中的数据,在一次draw call的执行过程中都是不变的;而在不同的draw call之间,我们可以修改其中的数据。它是我们把数据从CPU传递到GPU最常见的方法。constant buffer的概念和定义就不在这里赘述了,鄙文主要讨论如何优雅的管理constant buffer.

2. How to create and manipulate constant buffer gracefully.

Constant buffer在各种api中都有很好的支持,如在DX11中,以cbuffer为类型的闭包,可以定义一个constant buffer.

cbuffer可以根据你自己对它的功能或者buffer数据改变的策略来定义。在DX11中,我通过shader reflection的接口发现,没有在cbuffer闭包中的变量,都会被放到一个名叫@Globals的constant buffer中。当然这也取决你的fxc.exe的版本。其实让我们苦恼的并不是如何使用DX或者GL等这些api创建一个constant buffer对象,而是我们为了使用constant buffer,通常情况下需要在我们的引擎或者客户端的程序代码中,创建一个内存布局和大小与GPU端统一的数据结构。例如,我们要使用 transforms和change_every_frame这两个cbuffer,我们还要定义如下C的struct,和一些额外的代码。

这是一件很让人困扰的事情。首先,如果你开发了一个新的shader并使用了一个新的cbuffer的定义,那么你不得不修改你的引擎或者客户端代码。添加新的数据结构,还有使用和更新的代码。如果是这样,我们的程序或者引擎的扩展性就太差了!其次,你得非常小心的处理constant buffer内存布局的规则,否则你的数据不会正确的传递。例如,light_position后面要更一个float作为补位。我们应该把这些交给程序自己,而不是自己来做重复的工作。在C++中,我们必须要把这些与类型相关,也就是受限于编译期的,改成到运行期当中来计算。管理的基本方法如图所示:

726345-20160522235430982-157258810

 

将一个cbuffer分成的两个部分,一个大小跟GPU中cbuffer大小一致的memory block,和一个对cbuffer各个成员的描述meta data. 如何来实现呢?在最开始,我们需要一个枚举来描述所有的基本类型,例如float,float3,float4x4,这是从编译期转向运行期的第一步。

然后,我们需要一个结构体来描述整个constant buffer,例如change_every_frame这个cbuffer,我们要描述整个buffer的大小,light_color的data format,还有相对于cbuffer头地址的偏移量等。并且还要支持在cbuffer中使用结构体和数组。所以这个结构体应该是自递归的。如下面的代码

在编译shader之前,还需要多做一件事情,就是解析shader code中的cbuffer,把这些meta data都获取出来,创建好numeric_layout对象。当然,都已经解析了cbuffer,讲道理应该把整个shader codes都解析一遍,创建一个完整的effect框架。这部分的功能,我正在研究和开发中,希望能顺利完成并同大家分享。然后在渲染框架的与平台无关的代码部分,抽象一个constant buffer类型,并使用这个numeric_layout创建与平台无关的constant buffer对象。有了这个constant buffer对象,平台相关的代码就有足够多的信息正确创建设备上的cbuffer的对象了,无论是dx还是gl. 那么总体的流程如图:

726345-20160523001044826-737496802

在很多图形api中,对cbuffer的部分更新做的并不是很好,如只更新change_every_frame中的light_color分量。DX的UpdateSubresource无法实现,gl3.1之后有了ubo,才可以使用glSetBufferSubData来实现。在我们管理的cbuffer下,我们可以部分更新cpu中的cbuffer的memory,在一起update到gpu上来模拟这种部分更新的实现。

3. Use the powerful compile time computation of C++ to manipulate constant buffer

接下来聊聊如何利用C++新标准强大的编译期计算来方便我们创建cbuffer. 上述将编译期迁移到运行期的方法,很适合在渲染框架中使用。运行期化的代码,虽然能解决问题,但是创建的过程还是比较复杂。在写实验和测试的代码这类很小的程序的时候,上图的流程就显的笨重了。所以,我在实现numeric_layout的时候,提供了使用用户自定义类型来创建cbuffer的metadata的方法,以便小型程序使用。使用起来非常简单,代码如下:

首先定义自定义的cbuffer的结构体,跟shader code中的一模一样的结构体;然后使用boost::fusion做编译期的反射;最后使用numeric_layout的另外一个构造函数创建cbuffer的metadata,共cbuffer创建使用。大概的实现思路如下。

1. 使用large_class_wrapper<T>来避免不必要的栈内存分配。由于定义我们的constant buffer的数据结构往往是一个结构体,而我们创建numeric_layout对象并不需要这个结构体的实例或者实例的引用,只需要编译期的反射信息。我们没有必要去创建一个无用的对象,而是把类型传递给large_class_wrapper<T>类模板,让它带上类型信息,但是这个类模板本身是一个空类,大小为1,甚至会被编译期优化掉。所以这里使用large_class_wrapper<T>可以避免不比较的内存开销。

2. 然后,在使用被fusion适配成sequence之后的用户自定义类型T,对numeric_layout进行初始化。根据cbuffer的内存对齐规则,对sequence中的每一个成员类型做计算,当前成员类型计算的过程依赖上一个成员计算的结果。那么numeric_layout的构造函数调用的detail::init_variable_layout_from_tuple函数实现如下,

3. numeric_layout的构造函数,在编译期计算出了T在GPU中的大小,算法与运行期的原理基本相同,只是改写到编译期计算了。

目前还不支持struct中嵌套struct,我想不久的将来应该会支持的。仓库在这里,不过我写的很慢,连平台无关的代码都还没有写完。接下来会准备研究glslang,hlslcc,还有vulkan的SPIRV的一些库和tools的开源项目,来解决one shader all platforms的问题。

4. Tail

constant buffer曾经是我引擎开发工作当中一个比较痛苦的环节,每写一个shader,每多一个effect就得在C++代码中添加相应的数据结构和逻辑显得很DRY。而后错误的更新cbuffer招致的痛苦的调试过程也是历历在目。还有当我看到maya的cgfx如此灵活和强大功能更让我觉得cbuffer是得好好管理一下了。Powered by modern cpp and modern graphics api,希望我自己实现的渲染框架,可以在不添加一行C++代码的同时,还能高效的正确渲染新加入的effect,杜绝DRY的设计和实现。

将二进制数据嵌入json的几种方法

1. 在 xml 中嵌入二进制数据的几种方法

  1. 通过外部实体和标记法的方式表示二进制数据;
  2. 使用 MINE 数据类型来表示二进制数据(并把数据用 Base64 编码后放入CDATA节中);
  3. 将二进制数据嵌入 CDATA 节中,编码格式由用户自己定义。

其中,第一种方法可以使用 XML 中的 DTD 规范来指定一个外部的 dtd 文件;第二种方法是把二进制数据用 Base64 编码后,保存在 CDATA 节里;第三种方法,跟第二种类似,也是把数据保存在 CDATA 节里,不过编码的算法不一定使用 Base64,可以使用自己定义的编/解码算法。

XML 中 在 CDATA 节中写入 Base64 编码的格式如下:

2. xml 中 CDATA 的作用

在 XML 中:

所有 XML 文档中的文本均会被解析器解析,只有 CDATA 区段(CDATA section)中的文本会被解析器忽略。

由于 XML 跟 HTML 类似,主要使用 “<“, “>” 作为语言的标记,所以不能在 XML 文档中随意使用这些字符。

转义字符

非法的 XML 字符必须被替换为实体引用(entity reference)。

假如您在 XML 文档中放置了一个类似 “<” 字符,那么这个文档会产生一个错误,这是因为解析器会把它解释为新元素的开始。因此你不能这样写:

为了避免此类错误,需要把字符 “<” 替换为实体引用,就像这样:

xml_escape_chars

注释:严格地讲,在 XML 中只有字符 “<” 和 “&” 是非法的。单引号、引号和大于号是合法的,但是把它们替换为实体引用是个好的习惯,这可能跟 XML 大多数情况下还是用于 HTML 或别的 Web 服务有很大的关系。

所以,CDATA 的作用就是把其中包含的数据(文本)当作 raw 数据,不做任何解析,原样输出。

例如下面的 JavaScript 代码:

3. 其它语言中 raw 字符串的用法

3.1 在 PHP 中,这个用法叫做 Nowdoc 结构,具体格式是:

以 <<< 开头,后面紧跟单引号括起来的标识符,例如:

标识符 EOT 的名字是可以随意取的,结尾和开头的标识符对应即可,完整的例子:

PHP 中还有一个叫做 Heredoc 结构的东西,他跟 Newdoc 的区别是标识符没有单引号,包含的内容也不转义,但变量的值可以被替换。

3.2 C++ 11 中也有 raw 字符串的用法,格式是:

其中的 xxxxxxxx 就是不需要转义的内容,跟 PHP 的 Newdoc 类似,它可以包含回车换行,TAB字符等,例如:

其中的标识符 foo 可以是别的名字,也可以省略。

4. 在 json 中嵌入二进制数据

跟 XML 类似,JSON 的 string 类型里,也存在必须转义为实体引用的转义字符,下面是 JSON 中 string 的定义:

json_string_escape_chars

(以上图片截取自 http://www.json.org/json-zh.html

我们可以看到,除了 ” 和 \ 字符,以及各种控制字符(\n,\r,\t,\b,\f等),还有转义符 / 自己本身,还包括了 \u#### 这样的 UNICODE 字符表示法。以上这些转义,在 C++ 中,除了 / 字符的转义不支持以外,其它都适用于 C++。但是 C++ 不仅仅包括这些,还支持 ‘\0’,’\8’,’\x0d’, ‘\005′ 的用法,这是 JSON 里没有的。

我们要把二进制数据嵌入到 JSON 当中,就是把数据用 raw 的方式保存在一个 string 的值里面,JSON 中 string 的格式是:

可是,JSON 并不支持 raw string 的用法,那么我们就需要把二进制数据转换成纯文本的字符流,至于转换的算法是怎样的,接下来我们就来研究一下这个问题。

4.1 Base64

最常规的做法,就是把二进制数据转换成 Base64 格式,跟 XML 中的用法类似。

Base64 标准中的编码字符集:

Base64 的原理是把3个Byte(即 3 x 8bit = 24 bit)的数据转换为4个Byte(即 4 x 6bit = 24 bit),转换后的每一位Byte只用了前面的 6 个bit,即 2^6 = 64,这也是 Base64 名字的由来。如果原始数据的大小是 1.0,那么转换后的数据大小是 1.0 * (4 / 3) ≈ 1.333,也就是说转换后会比原数据大小多出约 0.333 倍。

在 JSON 中用 Base64 表示二进制数据,用法如下:

  • 优点:编码后的字符里只有 ‘/’ 字符是需要再转义的(而且只在 JSON 标准里才这么规定,很多 JSON 库并不对 ‘/’ 字符转义)。而且编码后的大小是固定的,可计算出来的,并且也不会很大。
  • 缺点:无。
  • 转换的难度:适中。虽然 Base64 使用的是大端表示法,这跟编码和解码带来了一些不便,但由于算法比较简单,编码和解码都可以通过位移和查表来完成,所以转换较容易,效率尚可。
  • 编码的大小:约为原数据的 1.333 倍。

4.2 十六进制表示法

很直接的,我们也会想到 HEX 这种格式,即常用的十六进制表示法。例如:

从某种意义上来讲,如果跟 Base64 相比,它也可以被称为 Base16,你可以把 ‘0-9′, ‘A-F’ 这 16 个字符看作是它的编码字符集,如下所示:

在 JSON 中用十六进制来表示二进制数据,是这样的:

  • 优点:每一个字节会被表示成两个字符,且编码后的字符里完全没有需要再转义的字符,而且编码和解码都可以使用最简单的位移和查表法完成,因此效率比较高。编码后的大小也是固定的,是原数据的两倍,不多也不少。
  • 缺点:因为是用空间换时间,所以转换后的大小是最大的,固定是原数据的两倍,虽然编码效率高,但用于网络传输时不太有利。
  • 转换的难度:低。编码和解码简单易懂,代码简单,执行速度也快。
  • 编码的大小:是原数据的 2.0 倍。

4.3 ‘\0′ 字符反转义

把二进制数据转换成字符串流,我们遇到的最大的麻烦,是要如何处理 ‘\0′ 这个字符,因为字符串以 ‘\0′ 作为终止符,所以这个字符肯定不能出现在编码后的字符串里,必须用某个字符或字符串代替。如果解决了这个问题,其它字符再按照 JSON 正常的转义转换,问题基本上就搞定了。

当在看 MessagePack 的相关讨论的时候,有人提到 “\u0000″ 这样的方式,所以我留意了一下这个方案。这里有个需要注意的地方是,”\u0000″ 并不是指 00 00 这样的两个字节,而是 Unicode 里 ‘\0′ 字符的表示法,其实它只表示 00 一个字节。

也许你会说,我也可以把 ‘\0′ 字符替换成任何想要的字符或字符串,比如 “<null>”(这也是很多人最早能想到的办法,我也一样)。可是如果我原来的二进制数据里本来就包含 “<null>” 这个字符串,该怎么办?这的确是我们担心的问题,为了避免冲突,我们只好把替换的字符串弄得复杂一点,可是这样始终解决不了问题,还是存在冲突的可能性。

为什么用 “\u0000″ 这样的字符串替换 ‘\0′ 不存在这个问题?如果原来的二进制数据里存在 “\u0000″ 这个字符串,由于数据存入 JSON string 的时候需要做一次转义,那么它就会变成 “\\u0000″,解码的时候就会还原成 “\u0000″,而不是 ‘\0’,而 “\u0000″ 则会还原成 ‘\0’,并不存在冲突。

实际上,除了 “\u0000″ 这种方案,我还考虑了直接转义为 “\0″(分别是 ‘\’ ‘0’ 两个字符)的方案,为什么呢?因为,最坏的情况下,例如你的二进制数据全部都是 00,那么此种情况下,编码后的大小将会是原始数据大小的 6 倍。也就是说,为了避免在编码的过程中可能要对缓冲区不断扩容,导致内存碎片和效率降低,我们必须在一开始就分配原始数据 6 倍大小的缓冲区,这有点太大了,而且很不科学(虽然 rapidjson 也是这么干的,但它那是没有办法的办法),因为大多数情况下我们实际只会用到其中的 1.05 ~ 1.1 倍左右的缓冲区大小。

把 ‘\0′ 字符直接转义成 ‘\’ ‘0’ 两个字符的方案,它类似于 C/C++ 的反转义,好处只有一个,就是缓冲区只需要分配为原来的 2 倍即可,处理逻辑也相对简单一些。但原生的 JSON 对 ‘\0′ 转义是不支持的,大多数 JSON 库也都没有考虑这种情况,替换成 “\u0000″ 则是 JSON 默认就支持的,通用性和兼容性比较好。这种方案牺牲的是通用性和兼容性,想要正确的解码,也只能用自己的库。这里说的兼容性,是因为 “\0″ 这种表示法,在 JSON 里是未定义行为,有可能被认为格式错误,或被忽略掉,也有可能只输出 “0” 这个字符,结果是未知的。

所以,如果你生成的 JSON 数据需要别的语言的 JSON 库或同一种语言的不同的 JSON 库交互,则应该采用 “\u0000″ 这种方案,否则的话,可以采用对 ‘\0′ 直接反转义的方式,这样效率更高一点,但使用别的库解析你的 JSON 结果的时候可能会有问题,不过不是一定会有问题,你可以测试一下。

这一切的根本原因,是因为 JSON 里没有定义一个像 XML 的 CDATA 那样的 raw string literal 表示法,当然我们可以自己给它定义和实现一个,但其它 JSON 库识别不了,没办法做到通用意义也不是很大。

在 JSON 中用字符转义来表示二进制数据,大概是这样的(这是 “\0″ 的方案,并且做了二次转义后的结果):

“\u0000″ 方案

  • 优点:如果二进制数据里需要转义的字符比较少,那么编码后所增加的数据长度也比较少,总体的编码长度是比较小的(视数据而定)。
  • 缺点:初始缓冲区必须分配为原始数据的 6 倍,浪费比较大。处理逻辑相对比较复杂,效率是三种方法里最低的,但差距也不是特别大。
  • 转换的难度:最难。编码/解码处理逻辑相对比较复杂,比下面的 “\0″ 方案还要稍微复杂一点,效率最低。
  • 编码的大小:视数据而定,一般的文件,大小约为原始文件的 1.05 ~ 1.10 倍左右,如果是两次转义,则会更大一点,约 1.10 ~ 1.20 倍左右,编码大小是三种方法中最小的。

“\0″ 反转义方案

  • 优点:如果二进制数据里需要转义的字符比较少,那么编码后所增加的数据长度也比较少,总体的编码长度是比较小的(视数据而定)。
  • 缺点:初始缓冲区必须分配为原始数据的 2 倍。处理逻辑相对比较复杂,效率是三种方法里最低的,但差距也不是特别大。
  • 转换的难度:较难。编码/解码处理逻辑相对比较复杂,效率不太高。
  • 编码的大小:视数据而定,一般的文件,大小约为原始文件的 1.05 ~ 1.10 倍左右,如果是两次转义,则会更大一点,约 1.10 ~ 1.20 倍左右,编码大小是三种方法中最小的。

4.4 三种方法的总结

综合来看,Base64 最全面,编码长度跟原数据相比增加不多,编码/解码速度也还可以(如果采用更高效的编码/解码库将会获得更好的效果);Hex 十六进制转换速度最快,但编码长度最大,鱼与熊掌不可兼得;’\0′ 反转义的方法,编码长度最小,但编码/解码速度相对最慢,但并没有慢到差一个数量级,如果对于数据传输大小有比较严苛的要求,可以考虑这种方案。

所以,如果没有太特别的要求,一般推荐使用 Base64 编码方案,通用性比较好。

下面是三种方案的比较(第三种方案分为了两种):

json_binary_compare

下面分别是十六进制表示法、Base64 以及 ‘\0′ 字符转义三种方案对一个约 17MB 的 PDF 文件编码/解码耗时的比较:

json_binary_benchmark_h

可以看到,十六进制法用的时间最短,Base64 其次,’\0′ 字符转义最慢,但相差都不算很大,以上测试不是很全面(每个算法都只测了一下,测试结果的摇摆性比较大),但大概已经可以看出差别。

5. 关于 ‘\0′ 反转义的二次转义

很重要也很不重要,我还没有理清楚怎么写。。。(待补)

6. 关于 MessagePack

MessagePack 是个什么东西?MessagePack 是一个高效的二进制序列化格式,它可以像 JSON 那样在各个语言间交换数据,但它比 JSON 更快、更小。

为什么小?

首先注意一点,MessagePack 其实是一个二进制序列化格式,它不是面向文本的,而 JSON 是面向文本流的,我们来看看 MessagePack 主页http://msgpack.org/ 上的一个演示列子:

messagepack_demo

我们可以看到,它没有了 “{” 和 “}”,0x80 代表是 element map,而 0x82 的意思是这个 element map 的元素个数是 2 个(它其实是叫 fixmap,固定大小 map,表示范围是 0x80 ~ 0x8F)。如果是元素个数更多的 element map 则会用 0xDE + 两个字节的元素个数表示,这叫做 map16,如果是元素个数超过两个字节的,则用 0xDF + 四个自己的元素个数来表示,这叫做 map32。

类似的,0xA0 表示 fixed string (fixstr),表示范围是从 0xA0 ~ 0xBF,可以表示字符串长度是从 0 ~ 31,超过这个范围的是使用 str8 (0xD9),str16 (0xDA),str32 (0xDB) 表示。

对于整型,则把 0x00 ~ 0x7F 保留了用来表示正整数 0 ~ 127。0xC2 表示逻辑 false,0xC3 表示逻辑 true。

对于字符串,虽然里面的内容还是跟 JSON 一样,原样的没变,但由于一开始就指定了长度,所以也不用头和尾都用引号包起来。由于这些举措,你可以看到,MessagePack 跟 JSON 相比,节省了不少字节,这就是它比 JSON 小的原因,当然也有可能小不了多少,但总体上会小一些,一般测试显示可以小 10% 左右。

关于 MessagePack 的格式定义,可以查阅其 github 上的具体定义:https://github.com/msgpack/msgpack/blob/master/spec.md

下面是部分定义的截图:

messagepack_overview

从上图也可以看到,MessagePack 处理二进制数据的方式,它分为 bin8、bin16、bin32,分别表示二进制数据长度为 0~255,256~65535,65535~(2^32-1),也就是说,格式是:

可以看到,MessagePack 处理二进制数据只是加了个标记和数据长度,数据是原样输出的,所以既快也方便。因为 rapidjson 的 issue 里有人提到了 JSON 嵌入二进制数据可以参考 MessagePack 的设计,这里拿来比较一下还是有意义的。因为 JSON 是纯文本流,所以不可能支持这种设计方式,除非它定义了 raw string literal。

为什么快?

前面我们也看到了,MessagePack 的格式是标志位 + 数据长度这样的方式,这样给处理 Value 对象时分配缓冲区带来了便利,JSON 里是无法预先知道后面的字符串到底有多长,到什么时候结束,现在在解析字符串的时候可以提前就知道了,如果字符串比较长,可以避免不断对字符串扩容带来的效率损失。当然也不止字符串,很多地方都会因此而获益,比如 map,array 的元素个数也是可以解析头部即可得知,同样对于缓冲区的分配和管理会带来不错的效果。

对于 integer 和 float 的格式,因为是二进制格式,所以它可以直接从内存里原样输出,不过可惜的是,对于 integer,它使用的大端格式,这对于一般的支持小端的 CPU 会浪费几条指令,也许是考虑到网络传输一般都是用大端格式的原样吧,其实实际使用中,支持小端格式的硬件比例更高一些。对于 float 则是标准的 IEEE754 表示法,而这个标准是小端格式的,因为这个是 CPU 直接支持的,所以格式相对是固定的。

所以,综上几点,在 C/C++ 里,MessagePack 肯定是要比 JSON 快一点的,至少快多少,不太好说,看具体的数据格式和大小。

并不一定快

但是也有一些例外,比如在 JS 中,因为 JSON 是原生支持的,MessagePack 是 JS 里写的库,所以在 JS 里,解析 MessagePack 反而会比解析 JSON 慢很多。这也很容易理解,如果 JS 中也原生支持 MessagePack 的话,那么相信还是 MessagePack 更快一些,毕竟 JS 的用户代码是不可能跟原生调用比的。与此类似的是,如果那些语言里不能原生支持 MessagePack 的话,而是在语言里用函数库自己实现 MessagePack 解析的话,效率和内存方面都是不可能跟原生支持相比的,也许这也是 MessagePack 没有被广泛应用的原因之一吧。

此外,MessagePack 是二进制格式,在某些语言里,也许会给解析和使用带来一定的麻烦,可能也是其中一个原因。

7. 关于 Bson

SON 这个格式是专门为 MongoDB 而开发的,类似 JSON 的一种二进制格式,不一定比 JSON 存储的文件小(有时候可能还大不少),优点是解释快(事实上部分格式的解析跟 MessagePack 比可能还是要慢一些)。

BSON 的官网:http://bsonspec.org

其格式定义可以参阅:http://bsonspec.org/spec.html

下面是部分格式定义:

bson_spec_non_terminals

其思想跟 MessagePack 非常类似的,对于 document,会记录其 element list 的元素个数,然后才是具体的 list 数据。字符串的处理也非常类似,integer 和 float 也差不多,不过对于 integer 的处理,它用的是小端格式(明智多了)。

不过 BSON 并没有像 MessagePack 那样对较常见的短字符串,小整型等做一些优化,即 Fixed 类型(固定大小类型),对于一个字符串,不管长度是多少,BSON 基本是一视同仁的,长度都是用一个 4 字节的整型表示,并且格式是小端格式,也许这样解析的代码简单一些,但并不见得就一定好,至少数据长度比 MessagePack 的 Fixed 类型多 4 个字节,因为很多时候,我们的 key 和 name,还有一些其它值类型,长度一般都是很小的,基本都会在 Fixed 类型能表示的范围内,所以节省的数据量还是比较可观的。

BSON 表示一个 6 个字节的字符串值,格式是这样的:

不过有一点是值得注意的,这里的 \x06 是 C/C++ 里还未转义之前的表示法,它在内存里实际上是 06 这样的一个字节而已,不要被蒙骗了。但是 BSON 一开始的 document 长度的确是按未转义之前的字符长度计算的,也就是整个 BSON 串的总字符个数,便于分配缓冲区。

上面的字符串如果在内存里,是这样的:

下面这个 JSON 转换为 BSON 后的格式是:

\x16\x00\x00\x00 是整个 BSON 字符串的长度,0x02 是 utf-8 string,也就是键值,它是不需要定义长度的,因为 BSON 认为键值的字符串不会特别长,可以省略,后面的 \x06\x00\x00\x00 是 world 的字符串长度,包含 ‘\0′ 字符,最后以一个 ‘\0′ 字符结尾。

BSON 跟 MessagePack 还是非常类似的,只不过 MessagePack 对一些长度比较段且较常见的数据做了一些特殊处理,以减少数据的长度。如果 BSON 一开始的 document 除了整个串的长度,像 MessagePack 那样还加一个元素个数更好一点,这对整个树的遍历是有一定的好处的。

BSON 中的二进制数据格式是这么定义的:

下面演示一下 BSON 是如果嵌入二进制数据的:

以上使用的 subtype \x02 Binary (Old) 格式,其实跟 MessagePack 非常类似,就是标识头 + 子类型 + 二进制数据(原样)+ ‘\0’,当然跟 MessagePack 有点区别的是,定义 key name 的时候,也必须定义为 Binary 格式,而 MessagePack 不用,定义为一般的字符串 key 即可。

从总体上来看,MessagePack 好像设计得更好一点,BSON 有些不足,但它追求的是代码和处理逻辑相对的简单,有兴趣的朋友可以做一下比较。

8. 思考

面对 JSON, MessagePack 和 BSON,我们到底应该选择文本流还是二进制流?或者使用 Protocol Buffer?也许应该好好弄清楚我们的需求,必须要在各种不同的语言间交互数据吗?这些语言都能很好的支持我们定义的数据格式吗?是否在意升级消息格式以后的维护成本?

如果说 MessagePack 和 BSON 相当于自带了 DSL 描述的Protocol Buffer,但却没有 DSL 中数据结构的内存布局顺序(解析以后,JSON 或 二进制 JSON 的元素一般用哈希表存储的,所以一般是没有顺序的)。虽然 JSON 非常灵活,但是 JSON 对于消息格式的变动,依然还是要修改服务器端和客户端的代码并且重新编译,除非那个变动本来就是你定义消息格式的时候就考虑到了的,这个对于 Protocol Buffer 也可以通过 options 来实现。JSON 是无状态的,但依然无法实现改动消息格式却不想重新编译和发行新版本的构想。

最理想的方式是在更改消息格式以后,不需要重新编译,服务器端和客户端也不用修改,目前可以实现的办法是用动态语言实现热更新的方式。也许还有别的更好的办法,方案还是有一些的,实现起来比较困难,只要能解决 ABI 二进制兼容问题就好办多了,有知道的朋友请不吝赐教。

9. 关于 Base64 编码/解码库

Base64 算法还是可以做一些优化的,国人以前有人研究过这个,可以参考:

代码优化之-Base64编码函数的极限优化挑战
http://blog.csdn.net/housisong/article/details/1711153

此外,还可以参考这个 github 仓库(老外写的),里面有 Plain 普通版本,以及 SSE3 和 AVX2 的优化版本。

https://github.com/aklomp/base64

10. 测试代码 github 仓库

以上三种方案的实现代码的 github 地址是:

https://github.com/shines77/json_binary

注:由于时间、精力、能力和知识面有限,难免会有错误和遗漏的地方,欢迎批评指正,代码也会慢慢完善。

11. 参考文章

将二进制数据嵌入 XML 文档的三种方法
https://www.ibm.com/developerworks/cn/xml/x-binary/

JSON的定义(JSON官网)
http://www.json.org/json-zh.html

XML CDATA
http://www.w3school.com.cn/xml/xml_cdata.asp

[PHP] String 字符串
http://php.net/manual/zh/language.types.string.php

[C++] string literal
http://en.cppreference.com/w/cpp/language/string_literal

新型序列化类库MessagePack,比JSON更快、更小的格式
http://blog.csdn.net/educast/article/details/12746239

BSON 格式解释
http://blog.csdn.net/hengyunabc/article/details/6897540

 

C++中怎么对野指针进行防护

一直从事C++底层库的开发,这里以监听模式来示例野指针的防护。底层通知上层,一种方式是,底层提供一个监听接口类,上层实现,然后注册下来,一般是有注册就有反注册,可是把下层安全压在上层使用者,期望他们在释放这个监听接口类之前总是进行反注册,这个就太不明智,那么我们就需要基于框架设计能防护野指针破坏,这里我们提供一个Guard机制。
Guard翻译过来的意思就是警卫,顾名思义就是用来防护的。先看其实现:

一个Guard对象正常的通过其构造函数实例化的时候,那我们就认为这个Guard是一个主Guard,m_host设置为true,其实例化一个GuardHelper对象,这个对象有一个引用计数的标识和标识这个主Guard是否有效的标识位。我们再来分析其析构函数,如果是主Guard,其在析构的时候,会将Guard有效标识为设置为false,当引用计数为0时,释放m_guard实例。这里介绍最关键的副Guard概念,借由主Guard构建的Guard都是副Guard,在复执构造函数和赋值中m_host都是false。为了方便使用使Guard可以转化为bool。
其实说到这,我们怎么来防护野指针的思路也就有了,在类型中实例化一个主Guard,在其他需要保存这个指针的地方,保存一份其主Gurad的副Guard,那么在这个指针析构的时候,主Guard也就析构了,那么其他的副Gurad的m_vaild值就为false,那么我们在使用这个指针时就可以知道这个指针已经是野指针了。
那么主Guard该放在那里了,放在库框架的基类再合适不过,这个就可以在整个框架中的指针就行防护。

使用Guard的时候一般都是这样,保存指针和其副Guard。

这种结构过于丑陋,这里我们提供一个包裹类,将其作成一个Guard指针,和平常的指针一样使用。

使用示例:

运行结果:

对上层的野指针就行了防护,使用Guard的唯一一个需要特别注意的就是一定要值传递,不然你会挂的很惨。

Copy Protected by Chetan's WP-Copyprotect.