维基百科中对 Zero-copy
的解释是
零拷贝技术是指计算机执行操作时,CPU不需要先将数据从某处内存复制到另一个特定区域。这种技术通常用于通过网络传输文件时节省CPU周期和内存带宽。
维基百科里提到的零拷贝是在硬件和操作系统层面的,而本文主要介绍的是Netty在应用层面的优化。不过需要注意的是,零拷贝并非字面意义上的没有内存拷贝,而是避免多余的拷贝操作,即使是系统层的零拷贝也有从设备到内存,内存到设备的数据拷贝过程。
Netty 的零拷贝体现在以下几个方面
ByteBuf
的 slice
操作并不会拷贝一份新的 ByteBuf
内存空间,而是直接借用原来的 ByteBuf
,只是独立地保存读写索引。
- Netty 提供了
CompositeByteBuf
类,可以将多个 ByteBuf
组合成一个逻辑上的 ByteBuf
。
- Netty 的
FileRegion
中包装了 NIO
的 FileChannel.transferTo()
方法,该方法在底层系统支持的情况下会调用 sendfile
方法,从而在传输文件时避免了用户态的内存拷贝。
- Netty 的
PooledDirectByteBuf
等类中封装了 NIO
的 DirectByteBuffer
,而 DirectByteBuffer
是直接在 jvm 堆外分配的内存,省去了堆外内存向堆内存拷贝的开销。
下面来简单介绍下这几种方式。
slice 分片
以下以 AbstractUnpooledSlicedByteBuf
为例讲解 slice
的零拷贝原理,至于内存池化的实现 PooledSlicedByteBuf
,因为内存池要通过引用计数来控制内存的释放,所以代码里会出现很多与本文主题无关的逻辑,这里就不拿来举栗子了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
|
AbstractUnpooledSlicedByteBuf(ByteBuf buffer, int index, int length) { super(length); checkSliceOutOfBounds(index, length, buffer); if (buffer instanceof AbstractUnpooledSlicedByteBuf) { this.buffer = ((AbstractUnpooledSlicedByteBuf) buffer).buffer; adjustment = ((AbstractUnpooledSlicedByteBuf) buffer).adjustment + index; } else if (buffer instanceof DuplicatedByteBuf) { this.buffer = buffer.unwrap(); adjustment = index; } else { this.buffer = buffer; adjustment = index; }
initLength(length); writerIndex(length); }
|
以上为 AbstractUnpooledSlicedByteBuf
类的构造函数,比较简单,就不详细介绍了。
下面来看看 AbstractUnpooledSlicedByteBuf
对 ByteBuf
接口的实现代码,以 getBytes
方法为例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| @Override public ByteBuf getBytes(int index, ByteBuffer dst) { checkIndex0(index, dst.remaining()); unwrap().getBytes(idx(index), dst); return this; }
@Override public ByteBuf unwrap() { return buffer; }
private int idx(int index) { return index + adjustment; }
|
这是 AbstractUnpooledSlicedByteBuf
重载的 getBytes
方法,可以看到 AbstractUnpooledSlicedByteBuf
是直接在封装的 ByteBuf
上取的字节,但是重新计算了索引,加上了相对偏移量。
CompositeByteBuf
在有些场景里,我们的数据会分散在多个 ByteBuf
上,但是我们又希望将这些 ByteBuf
聚合在一个 ByteBuf
里处理。这里最直观的想法是将所有 ByteBuf
的数据拷贝到一个 ByteBuf
上,但是这样会有大量的内存拷贝操作,产生很大的CPU开销。
而 CompositeByteBuf
可以很好地解决这个问题,正如名字一样,这是一个复合 ByteBuf
,内部由很多的 ByteBuf
组成,但 CompositeByteBuf
给它们做了一层封装,可以直接以 ByteBuf
的接口操作它们。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51
|
private int addComponent0(boolean increaseWriterIndex, int cIndex, ByteBuf buffer) { assert buffer != null; boolean wasAdded = false; try { checkComponentIndex(cIndex);
int readableBytes = buffer.readableBytes();
@SuppressWarnings("deprecation") Component c = new Component(buffer.order(ByteOrder.BIG_ENDIAN).slice()); if (cIndex == components.size()) { wasAdded = components.add(c); if (cIndex == 0) { c.endOffset = readableBytes; } else { Component prev = components.get(cIndex - 1); c.offset = prev.endOffset; c.endOffset = c.offset + readableBytes; } } else { components.add(cIndex, c); wasAdded = true; if (readableBytes != 0) { updateComponentOffsets(cIndex); } } if (increaseWriterIndex) { writerIndex(writerIndex() + buffer.readableBytes()); } return cIndex; } finally { if (!wasAdded) { buffer.release(); } } }
|
这是添加一个新的 ByteBuf
的逻辑,核心是 offset
和 endOffset
,分别指代一个 ByteBuf
在 CompositeByteBuf
中开始和结束的索引,它们唯一标记了这个 ByteBuf
在 CompositeByteBuf
中的位置。
弄清楚了这个,我们会发现上面的代码无外乎做了两件事:
- 把
ByteBuf
封装成 Component
加到 components
合适的位置上
- 使
components
里的每个 Component
的 offset
和 endOffset
值都正确
下面来看看 CompositeByteBuf
对 ByteBuf
接口的实现代码,同样以 getBytes
方法为例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47
| @Override public CompositeByteBuf getBytes(int index, ByteBuf dst, int dstIndex, int length) { checkDstIndex(index, length, dstIndex, dst.capacity()); if (length == 0) { return this; }
int i = toComponentIndex(index); while (length > 0) { Component c = components.get(i); ByteBuf s = c.buf; int adjustment = c.offset; int localLength = Math.min(length, s.capacity() - (index - adjustment)); s.getBytes(index - adjustment, dst, dstIndex, localLength); index += localLength; dstIndex += localLength; length -= localLength; i ++; } return this; }
public int toComponentIndex(int offset) { checkIndex(offset);
for (int low = 0, high = components.size(); low <= high;) { int mid = low + high >>> 1; Component c = components.get(mid); if (offset >= c.endOffset) { low = mid + 1; } else if (offset < c.offset) { high = mid - 1; } else { return mid; } }
throw new Error("should not reach here"); }
|
可以看到 CompositeByteBuf
在处理 index
时是先将其转换成对应 Component
在 components
中的索引,以及在 Component
中的偏移,然后从这个 Component
的这个偏移开始,往后循环取字节,直到读完。
NOTE:这里有个小trick,因为 components
是有序排列的,所以 toComponentIndex
做索引转换时没有直接遍历,而是用的二分查找。
本文整理自
Netty 之 Zero-copy 的实现(上)
仅做个人学习总结所用,遵循CC 4.0 BY-SA版权协议,如有侵权请联系删除!