simple-binary-encoding


原文链接 https://mechanical-sympathy.blogspot.com/2014/05/simple-binary-encoding.html
github https://github.com/real-logic/simple-binary-encoding
wiki https://github.com/real-logic/simple-binary-encoding/wiki

金融系统通过以多种不同格式发送和接收大量消息进行通信。当人们使用像“巨大”这样的词时,我通常会想,“真的……有多少?” 因此,让我们量化金融业的“巨大”。来自金融交易所的市场数据馈送通常每秒可发出数万或数十万条消息,而像OPRA这样的聚合馈送峰值可达到每秒超过 1000 万条消息,并且数量逐年增长。此演示文稿提供了一个很好的概述

在这个疯狂的世界中,我们仍然看到大量使用 ASCII 编码的表示形式,例如FIX标签值,以及一些稍微健全的二进制编码表示形式,例如 FAST. 有些市场甚至犯了将市场数据作为 XML 发送的罪行!好吧,我不能抱怨太多,因为他们有时为我提供了编写超快速 XML 解析器的可观收入。

去年,作为 FIX社区成员的 CME委托 29West LBM 名气的Todd Montgomery和我自己构建新的 FIX 简单二进制编码的参考实现(SBE) 标准。SBE 是一种编解码器,旨在解决低延迟交易中的效率问题,特别关注市场数据。CME 在 FIX 社区内工作,在提出如此高效的编码演示方面做得非常出色。对于过去的 FIX 标签值实现的罪恶,也许是一个合适的赎罪。Todd 和我致力于 Java 和 C++ 的实现,后来我们在 .Net 方面得到了Adaptive出色的Olivier Deheurles的帮助。与这样的团队一起解决一个很酷的技术问题是一项梦寐以求的工作。SBE 概述 SBE 是一种OSI

用于以二进制格式编码/解码消息的第 6 层表示,以支持低延迟应用程序。在我描述的存在性能问题的许多应用程序中,消息编码/解码通常是最重要的成本。我见过许多应用程序在解析和转换 XML 和 JSON 上花费的 CPU 时间比执行业务逻辑要多得多。SBE 旨在使系统的这一部分尽可能高效。SBE 遵循许多设计原则来实现这一目标。遵守这些设计原则有时意味着不会提供其他编解码器中可用的功能。例如,许多编解码器允许在消息中的任何字段位置对字符串进行编码;SBE 仅允许可变长度字段(例如字符串)作为在消息末尾分组的字段。

SBE 参考实现由一个编译器组成,该编译器将消息模式作为输入,然后生成特定于语言的存根。存根用于直接对来自缓冲区的消息进行编码和解码。SBE 工具还可以生成模式的二进制表示,可用于动态环境中消息的动态解码,例如日志查看器或网络嗅探器。

设计原则推动了编解码器的实现,确保消息通过内存流式传输,而无需回溯、复制或不必要的分配。内存访问模式在高性能应用程序的设计中不应低估。任何语言的低延迟系统尤其需要考虑所有分配,以避免在回收中产生问题。这适用于托管运行时和本机语言。SBE 在所有三种语言实现中都是完全免费的。

应用这些设计原则的最终结果是编解码器的吞吐量比 Google 协议缓冲区 (GPB) 高约 16-25 倍,并且延迟非常低且可预测。这已在微基准测试和实际应用程序使用中观察到。一个典型的市场数据消息可以在大约 25ns 内进行编码或解码,而在相同硬件上使用 GPB 的相同消息大约需要 1000ns。XML 和 FIX 标记值消息再次慢了几个数量级。

SBE 的最佳选择是作为结构化数据的编解码器,这些数据主要是固定大小的字段,即数字、位集、枚举和数组。虽然它确实适用于字符串和 blob,但我发现一些限制是可用性问题。这些用户最好使用更适合字符串编码的编解码器。

消息结构

消息必须能够按顺序读取或写入,以保持流式访问设计原则,即无需回溯。一些编解码器为必须间接访问的可变长度字段(例如字符串类型)插入位置指针。这种间接的代价是额外的指令加上失去硬件预取器的支持。SBE 的设计允许纯顺序访问和无副本本机访问语义。

img
图1

SBE 消息有一个公共标头,用于标识要遵循的消息正文的类型和版本。标头后面是消息的根字段,它们都是固定长度和静态偏移的。根字段与 C 中的结构非常相似。如果消息更复杂,则可以跟随一个或多个类似于根块的重复组。重复组可以嵌套其他重复组结构。最后,可变长度的字符串和 blob 出现在消息的末尾。字段也可以是可选的。可以在 此处找到描述 SBE 表示的 XML 模式。

SbeTool 和编译器

要使用 SBE,首先需要为您的消息定义一个模式。SBE 提供了一个独立于语言的类型系统,支持整数、浮点数、字符、数组、常量、枚举、位集、复合、重复的分组结构以及可变长度的字符串和 blob。

可以将消息模式输入SbeTool并进行编译以生成多种语言的存根,或生成适用于即时解码消息的二进制元数据。

java [-Doption=value] -jar sbe.jar <message-declarations-file.xml>

SbeTool 和编译器是用 Java 编写的。该工具目前可以在 Java、C++ 和 C# 中输出存根。

使用存根编程 可以在 此处找到在带有支持代码的模式

中定义的消息的完整示例 。生成的存根遵循享元模式,重复使用实例以避免分配。存根在偏移处包装缓冲区,然后按顺序和本机读取它。

// 先写消息头
MESSAGE_HEADER.wrap(directBuffer, bufferOffset, messageTemplateVersion) 
              .blockLength(CAR.sbeBlockLength()) 
              .templateId(CAR.sbeTemplateId()) 
              .schemaId(CAR.sbeSchemaId()) 
              .version(CAR.sbeSchemaVersion ()); 

// 然后写入消息体
car.wrapForEncode(directBuffer, bufferOffset) 
   .serialNumber(1234) 
   .modelYear(2013) 
   .available(BooleanType.TRUE ) .code 
   (Model.A) 
   .putVehicleCode(VEHICLE_CODE, srcOffset);

可以通过生成的存根以流畅的方式编写消息。每个字段显示为一对生成的编码和解码方法。

// 读取头部并查找合适的模板来解码
MESSAGE_HEADER.wrap(directBuffer, bufferOffset, messageTemplateVersion); 

final int templateId = MESSAGE_HEADER.templateId(); 
final int actuatorBlockLength = MESSAGE_HEADER.blockLength(); 
final int schemaId = MESSAGE_HEADER.schemaId(); 
最终 int actingVersion = MESSAGE_HEADER.version(); 

// 一旦找到模板,就可以对字段进行解码。
car.wrapForDecode(directBuffer,bufferOffset,actingBlockLength,actingVersion);

final StringBuilder sb = new StringBuilder(); 
sb.append("\ncar.templateId=").append(car.sbeTemplateId()); 
sb.append("\ncar.schemaId=").append(schemaId);
sb.append("\ncar.schemaVersion=").append(car.sbeSchemaVersion()); 
sb.append("\ncar.serialNumber=").append(car.serialNumber()); 
sb.append("\ncar.modelYear=").append(car.modelYear()); 
sb.append("\ncar.available=").append(car.available()); 
sb.append("\ncar.code=").append(car.code());

在所有语言中生成的代码提供的性能类似于在内存上强制转换 C 结构。

动态解码

编译器为输入 XML 消息模式生成中间表示 (IR)。此 IR 可以以 SBE 二进制格式序列化,以用于稍后对已存储的消息进行动态解码。它对于不会与存根一起编译的工具(例如网络嗅探器)也很有用。可以在此处找到使用的 IR 的完整示例。

Direct Buffers

SBE,通过 Agrona,通过 MutableDirectBuffer类为 Java 提供抽象,以使用 byte[]、heap 或直接ByteBuffer的缓冲区 缓冲区,以及从Unsafe.allocateMemory(long)或 JNI 返回的堆外内存地址。在低延迟应用程序中,消息通常通过MappedByteBuffer在内存映射文件中进行编码/解码,因此可以由内核[传输](http://docs.oracle.com/javase/7/docs/api/java/nio/channels/FileChannel.html#transferTo(long, long, java.nio.channels.WritableByteChannel))到网络通道,从而避免用户空间复制。

C++ 和 C# 具有对直接内存访问的内置支持,并且不需要像 Java 版本那样的抽象。为 C# 添加了 DirectBuffer 抽象以支持 Endianess 并封装不安全的指针访问。

消息扩展和版本控制

SBE 模式带有允许消息扩展的版本号。可以通过在块末尾添加字段来扩展消息。为了向后兼容,不能删除或重新排序字段。

扩展字段必须是可选的,否则读取旧消息的较新模板将不起作用。模板携带 min、max、null、timeunit、字符编码等元数据,这些可通过存根上的静态(类级别)方法访问。

字节排序和对齐

消息模式允许通过指定偏移量来精确对齐字段。字段默认以 Little Endian编码除非在模式中另有说明,否则形式。为了获得最佳性能,应使用字对齐边界上的字段的本机编码。在某些处理器上访问非对齐字段的代价可能非常大。对于对齐,必须考虑帧协议和内存中的缓冲区位置。

消息协议

我经常看到人们抱怨编解码器无法支持单个消息中的特定演示。然而,这通常可以通过消息协议来解决。协议是将交互拆分为其组成部分的好方法,这些部分通常可以组合用于系统之间的许多交互。例如,模式元数据的 IR 实现比单个消息的结构所能支持的更复杂。我们通过首先发送一个提供概述的模板消息来编码 IR,然后是一个消息流,每个消息都对来自编译器 IR 的标记进行编码。这允许设计一个非常快速的 OTF 解码器,它可以实现为一个线程解释器,其分支比典型的基于开关的状态机少得多。

协议设计是大多数开发人员似乎没有机会学习的领域。我觉得这是一个很大的损失。如此多的开发人员将诸如 ASCII 之类的“编码”称为“协议”这一事实非常有说服力。当一个人与像 Todd 这样一生都在成功设计协议的程序员一起工作时,协议的价值就显而易见了。

存根性能

与动态 OTF 解码相比,存根提供了显着的性能优势。对于访问原始字段,我们相信性能已达到通用工具所能达到的极限。生成的汇编代码与编译器生成的用于访问 C 结构的代码非常相似,即使是从 Java 中也是如此!

关于存根的一般性能,我们观察到 C++ 与 Java 相比具有非常小的优势,我们认为这是由于运行时插入的安全点检查。C# 版本稍微落后一点,因为它的运行时没有像 Java 运行时那样积极地使用内联方法。所有三种语言的存根都能够在数十纳秒内对典型的金融消息进行编码或解码。相对于应用程序逻辑的其余部分,这有效地使大多数应用程序的消息编码和解码几乎免费。

反馈

这是 SBE 的第一个版本,我们欢迎反馈. 参考实现受 FIX 社区规范的约束。可能会影响规范,但请不要期望会接受明显违反规范的拉取请求。已经讨论了对 Javascript、Python、Erlang 和其他语言的支持,非常受欢迎。

更新:2014 年 5 月 8 日

感谢 GPB 的创建者 Kenton Varda 的反馈,我们能够改进基准以从 GPB 中获得最佳性能。以下是 Java 基准测试的更改结果。

与初始结果相比,有关优化的 C++ GPB 示例显示吞吐量大约翻了一番。应该注意的是,与 C++ 相比,在 Java 中使用 GPB 时,您通常必须做相反的事情才能获得性能改进,例如分配对象而不是重用它们。

GPB优化前:

Mode Thr Cnt Sec Mean Mean error Units 
     [exec] ucrprotobuf.CarBenchmark.testDecode thrpt 1 30 1 462.817 6.474 ops/ms 
     [exec] ucrprotobuf.CarBenchmark.testEncode thrpt 1 30 1 326.018 2.972 ops/ms 
     [exec] ucrprotobuf.MarketDataBenchmark.testDecode thrpt 1 30 1 1148.050 17.194 ops/ms 
     [exec] ucrprotobuf.MarketDataBenchmark.testEncode thrpt 1 30 1 1242.252 12.248 ops/ms 

     [exec] ucrsbe.CarBenchmark.testDecode thrpt 1 30 1 10436.476 102ucrsbe.ms 
     [exec] testEncode thrpt 1 30 1 11657.190 65.168 操作/毫秒
     [exec] ucrsbe.MarketDataBenchmark.testDecode thrpt 1 30 1 34078.646 261.775 ops/ms 
     [exec] ucrsbe.MarketDataBenchmark.testEncode thrpt 1 30 1 29193.600 443.638 ops/ms

GPB优化后:

Mode Thr Cnt Sec Mean Mean error Units 
     [exec] ucrprotobuf.CarBenchmark.testDecode thrpt 1 30 1 619.467 4.429 ops/ms 
     [exec] ucrprotobuf.CarBenchmark.testEncode thrpt 1 30 1 433.711 10.364 ops/ms 
     [exec] ucrprotobuf.MarketDataBenchmark.test thrpt 1 30 1 2088.998 60.619 ops/ms 
     [exec] ucrprotobuf.MarketDataBenchmark.testEncode thrpt 1 30 1 1316.123 19.816 ops/ms
吞吐量 msg/ms - 在 GPB 优化之前
测试协议缓冲区SBE比率
汽车编码462.81710436.47622.52
汽车解码326.01811657.19035.76
市场数据编码1148.05034078.64629.68
市场数据解码1242.25229193.60023.50
吞吐量 msg/ms - GPB 优化后
测试协议缓冲区SBE比率
汽车编码619.46710436.47616.85
汽车解码433.71111657.19026.88
市场数据编码2088.99834078.64616.31
市场数据解码1316.12329193.60022.18

文章作者: Kevin
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Kevin !
评论
  目录