hank-whu / turbo-rpc

turbo is a super fast reactive rpc framework

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

关于序列化/反序列化部分zero-copy

fengjiachun opened this issue · comments

建议zero-copy和byte[]两种方式共存, 在大整数较多的情况下writeVarint/readVarint不一定占优势
另外调用unsafe.getXXX/setXXX次数在超过一定阈值后效果可能不如一次memorycopy+byte[]纯数组读写好

commented

感谢分享,有具体的测试用例吗?我研究下

下面链接是和jupiter代码强相关的benchmark代码, 可以参考下(可以调整下序列化对象中Integer.MAX_VALUE, Integer.MIN_VALUE这种值得个数, 多跑几次)

https://github.com/fengjiachun/Jupiter/blob/master/jupiter-example/src/main/java/org/jupiter/benchmark/serialization/SerializationBenchmark.java

本来jupiter中是没有支持zero-copy的, 看了turbo的代码受到的启发就增加了对zero-copy的支持(LowCopyProtocolDecoder.java/LowCopyProtocolEncoder.java), 但目前两种方式在jupiter**存, 因为我测试下来这两个方式谁快谁慢取决于不同的被序列化对象

commented

你的意思是 ZigZag 算法在数字不同的情况下性能差别很大?
我之前测试过 bytebuf 和 byte[] 写入读取没太大性能差别

@hank-whu ZigZag在turbo也只有在reaad/writeSInt32这种方法里用了, read/writeInt32这种protostuff调用最多的方法没有使用ZigZag

两者性能确实不会差太多, 我测试也就10%上下, 谁高谁低取决于数字是怎么组合的,
数字较大的情况下在你的writeVarLong里需要调用writeInt两次, 再调用writeByte一次, 我的的意思是unsafe.setXX的调用频次更多了, 相比memoryCopy后的直接操作数组, 综合下来zero-copy可能并没有变快, 反而慢了一点点, 在jupiter里我的测试结果是这样的, turbo我不能确定, 毕竟我们实现方式不完全一样

commented

我看了下你的测试场景,多少存在点问题的。

    @Benchmark
    public void protoStuffBytesArray() {
        byte[] bytes = protoStuffSerializer.writeObject(createUsers(USER_COUNT));
        ByteBuf byteBuf = allocator.buffer(bytes.length);
        byteBuf.writeBytes(bytes);
        byteBuf.release();
        protoStuffSerializer.readObject(bytes, Users.class);
    }

你的 byte[] 被复用了,实际场景应该是要被创建复制两次的,下面的代码才符合实际:

    @Benchmark
    public void protoStuffBytesArray() {
    	//写入
        byte[] bytes = protoStuffSerializer.writeObject(createUsers(USER_COUNT));
        ByteBuf byteBuf = allocator.buffer(bytes.length);
        byteBuf.writeBytes(bytes);
        
        //读取
        bytes = new byte[byteBuf.readableBytes()];
        byteBuf.readBytes(bytes);
        protoStuffSerializer.readObject(bytes, Users.class);
        
        //释放
        byteBuf.release();
    }

发送端申请 byte[], 序列化写入 byte[], byte[] 复制数据到 ByteBuf
接收端申请 byte[], ByteBuf 复制数据到 byte[], 反序列化读取数据

在这个测试场景下直接操作 ByteBuf 性能略好一点。
此外 ZigZag 的写入优化也是有效果的,你可以测试一下。

不过总体而言,这个地方的优化对最终的性能其实起不到啥用处的。
减小消息体、减少锁竞争、减少线程切换、优化netty参数 其实更有效果。

恩恩, 你纠正的对, 是错误的复用了bytes
ZigZag是 (n << 1) ^ (n >> 31) 与 (n << 1) ^ (n >> 63) 啊, 你给的链接是关于VarInt的

commented

嗯,可能是我没把这两个概念区分开,当成一回事来思考了。
我的意思是这种变长编码算法,有优化空间:可以把每次写入一个 byte,优化为尽量批量写入。
另外 byte[] 使用 Unsafe 写入 int,和 ByteBuf 写入 int 本质上是一样的。
具体的实现细节导致性能会有一点差异(ByteBuf 写入要做更多的校验),不过差异不会太大。

commented

整数变长编码算法有一个基本的假设:大部分数字都是小数字。
如果这个假设不成立,性能是会变得更差的。

commented

数字较大的情况下在你的writeVarLong里需要调用writeInt两次, 再调用writeByte一次


这个应该是:写入一次 long,再写入一次 byte。
偷懒了,后面应该优化一下。

恩, 我也在jupiter尝试一下, 把writeVarInt再优化一把