简单的二进制编码

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

在这个疯狂的世界中,我们仍然看到大量使用ASCII编码的表示形式,例如FIX标签值,以及一些更合理的二进制编码的表示形式,例如FAST 。 有些市场甚至犯下了以XML发送市场数据的罪过! 好吧,我不能抱怨太多,因为有时它们为我提供了编写超快速XML解析器的丰厚收益。

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

SBE概述

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

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

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

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

SBE的最佳选择是作为结构化数据的编解码器,结构化数据通常是固定大小的字段,包括数字,位集,枚举和数组。 尽管它确实适用于字符串和Blob,但我发现许多限制是可用性问题。 这些用户最好使用另一个更适合于字符串编码的编解码器。

讯息结构

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

SBE-msg格式
图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#输出存根。

存根编程

这里可以找到带有支持代码的模式中定义的消息的完整示例。 生成的存根遵循flyweight模式,实例被重用以避免分配。 存根将缓冲区包装在偏移处,然后按顺序本机读取。

// Write the message header first
    MESSAGE_HEADER.wrap(directBuffer, bufferOffset, messageTemplateVersion)
                  .blockLength(CAR.sbeBlockLength())
                  .templateId(CAR.sbeTemplateId())
                  .schemaId(CAR.sbeSchemaId())
                  .version(CAR.sbeSchemaVersion());

    // Then write the body of the message
    car.wrapForEncode(directBuffer, bufferOffset)
       .serialNumber(1234)
       .modelYear(2013)
       .available(BooleanType.TRUE)
       .code(Model.A)
       .putVehicleCode(VEHICLE_CODE, srcOffset);

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

// Read the header and lookup the appropriate template to decode
    MESSAGE_HEADER.wrap(directBuffer, bufferOffset, messageTemplateVersion);

    final int templateId = MESSAGE_HEADER.templateId();
    final int actingBlockLength = MESSAGE_HEADER.blockLength();
    final int schemaId = MESSAGE_HEADER.schemaId();
    final int actingVersion = MESSAGE_HEADER.version();

    // Once the template is located then the fields can be decoded.
    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的完整示例。

直接缓冲区

SBE通过DirectBuffer类为Java提供了一种抽象,以与byte []缓冲区,堆缓冲区或直接ByteBuffer缓冲区以及从Unsafe.allocateMemory(long)或JNI返回的堆外内存地址一起使用 。 在低延迟应用程序中,消息通常通过MappedByteBuffer在内存映射文件中进行编码/解码,因此内核可以将其传输到网络通道,从而避免了用户空间的复制。

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

邮件扩展和版本控制

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

扩展字段必须是可选的,否则使用更新的模板读取较旧的消息将不起作用。 模板带有用于最小,最大,空,时间单位,字符编码等的元数据,可以通过存根上的静态(类级别)方法访问这些元数据。

字节排序和对齐

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

讯息通讯协定

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

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

存根性能

与动态OTF解码相比,存根提供了显着的性能优势。 对于访问原始字段,我们认为性能已达到通用工具所能达到的极限。 生成的汇编代码与编译器为访问C结构(甚至从Java)生成的汇编代码非常相似。

关于存根的一般性能,我们发现C ++比Java具有非常小的优势,我们认为这是由于在运行时插入了Safepoint检查所致。 由于其运行时在内联方法方面不如Java运行时那样积极,因此C#版本落后了一些。 所有这三种语言的存根都能在数十纳秒内编码或解码典型的财务消息。 相对于其余应用程序逻辑,这对于大多数应用程序实际上使消息的编码和解码几乎免费。

反馈

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

翻译自: https://www.javacodegeeks.com/2014/05/simple-binary-encoding.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值