Unsafe與ByteBuffer那些事
上一篇文章《聊聊Unsafe的一些使用技巧》寫作之后,閱讀量很快超過了 1500,Kirito 在這里感謝大家的閱讀啦,所以我又來更新了。如果你還沒有閱讀上一篇文章,我建議你先去看下,閑話不多說,開始今天的話題。
無論是日常開發(fā)還是競(jìng)賽,Unsafe 不常有而 ByteBuffer 常有,只介紹 Unsafe,讓我的博文顯得很“炫技”,為了證明“Kirito的技術(shù)分享”它可是一個(gè)正經(jīng)的公眾號(hào),所以這篇文章會(huì)說到另一個(gè)比較貼地氣的主角 ByteBuffer。我會(huì)把我這么多年打比賽的經(jīng)驗(yàn)傳授給你,只求你的一個(gè)三連。
從 DirectBuffer 的構(gòu)造器說起
書接上文,我提到過 DirectBuffer 開辟的堆外內(nèi)存其實(shí)就是通過 Unsafe 分配的,但沒有詳細(xì)介紹,今天就給他補(bǔ)上。看一眼 DirectBuffer 的構(gòu)造函數(shù)
????DirectByteBuffer(int?cap)?{???????????????????//?package-private
????????super(-1,?0,?cap,?cap);
????????boolean?pa?=?VM.isDirectMemoryPageAligned();
????????int?ps?=?Bits.pageSize();
????????long?size?=?Math.max(1L,?(long)cap?+?(pa???ps?:?0));
????????Bits.reserveMemory(size,?cap);
????????long?base?=?0;
????????try?{
????????????base?=?unsafe.allocateMemory(size);
????????}?catch?(OutOfMemoryError?x)?{
????????????Bits.unreserveMemory(size,?cap);
????????????throw?x;
????????}
????????unsafe.setMemory(base,?size,?(byte)?0);
????????if?(pa?&&?(base?%?ps?!=?0))?{
????????????//?Round?up?to?page?boundary
????????????address?=?base?+?ps?-?(base?&?(ps?-?1));
????????}?else?{
????????????address?=?base;
????????}
????????cleaner?=?Cleaner.create(this,?new?Deallocator(base,?size,?cap));
????????att?=?null;
????}
短短的十幾行代碼,蘊(yùn)含了非常大的信息量,先說關(guān)鍵點(diǎn)
long base = unsafe.allocateMemory(size);調(diào)用 Unsafe 分配內(nèi)存,返回內(nèi)存首地址unsafe.setMemory(base, size, (byte) 0);初始化內(nèi)存為 0。這一行我們放在下節(jié)做重點(diǎn)介紹。Cleaner.create(this, new Deallocator(base, size, cap));設(shè)置堆外內(nèi)存的回收器,不詳細(xì)介紹了,可以參考我之前的文章《一文探討堆外內(nèi)存的監(jiān)控與回收》。
僅構(gòu)造器中的這一幕,便讓 Unsafe 和 ByteBuffer 產(chǎn)生了千絲萬縷的關(guān)聯(lián),發(fā)揮想象力的話,可以把 ByteBuffer 看做是 Unsafe 一系列內(nèi)存操作 API 的 safe 版本。而安全一定有代價(jià),在編程領(lǐng)域,一般都有一個(gè)常識(shí),越是接近底層的事物,控制力越強(qiáng),性能越好;越接近用戶的事物,更易操作,但性能會(huì)差強(qiáng)人意。ByteBuffer 封裝的 limit/position/capacity 等概念,用熟悉了之后我覺得比 Netty 后封裝 的 ByteBuf 還要簡(jiǎn)便,但即使優(yōu)秀如它,仍然有被人嫌棄的一面:大量的邊界檢查。
一個(gè)最吸引性能挑戰(zhàn)賽選手去使用 Unsafe 操作內(nèi)存,而不是 ByteBuffer 地方,便是邊界檢查。如示例代碼一:
public?ByteBuffer?put(byte[]?src,?int?offset,?int?length)?{
????if?(((long)length?<0)?>?Bits.JNI_COPY_FROM_ARRAY_THRESHOLD)?{
????????checkBounds(offset,?length,?src.length);
????????int?pos?=?position();
????????int?lim?=?limit();
????????assert?(pos?<=?lim);
????????int?rem?=?(pos?<=?lim???lim?-?pos?:?0);
????????if?(length?>?rem)
????????????throw?new?BufferOverflowException();
????????????Bits.copyFromArray(src,?arrayBaseOffset,
???????????????????????????????(long)offset?<0,
???????????????????????????????ix(pos),
???????????????????????????????(long)length?<0);
????????position(pos?+?length);
????}?else?{
????????super.put(src,?offset,?length);
????}
????return?this;
}
你不用關(guān)心上述這段代碼在 DirectBuffer 中充當(dāng)著什么作用,我想展示給你的僅僅是它的 checkBounds 和 一堆 if/else,尤其是追求極致性能的場(chǎng)景,極客們看到 if/else 會(huì)神經(jīng)敏感地意識(shí)到分支預(yù)測(cè)的性能下降,第二意識(shí)是這坨代碼能不能去掉。
如果你不希望有一堆邊界檢查,完全可以借助 Unsafe 實(shí)現(xiàn)一個(gè)自定義的 ByteBuffer,就像下面這樣。
public?class?UnsafeByteBuffer?{
????private?final?long?address;
????private?final?int?capacity;
????private?int?position;
????private?int?limit;
????public?UnsafeByteBuffer(int?capacity)?{
????????this.capacity?=?capacity;
????????this.address?=?Util.unsafe.allocateMemory(capacity);
????????this.position?=?0;
????????this.limit?=?capacity;
????}
????public?int?remaining()?{
????????return?limit?-?position;
????}
????public?void?put(ByteBuffer?heapBuffer)?{
????????int?remaining?=?heapBuffer.remaining();
????????Util.unsafe.copyMemory(heapBuffer.array(),?16,?null,?address?+?position,?remaining);
????????position?+=?remaining;
????}
????public?void?put(byte?b)?{
????????Util.unsafe.putByte(address?+?position,?b);
????????position++;
????}
????public?void?putInt(int?i)?{
????????Util.unsafe.putInt(address?+?position,?i);
????????position?+=?4;
????}
????public?byte?get()?{
????????byte?b?=?Util.unsafe.getByte(address?+?position);
????????position++;
????????return?b;
????}
????public?int?getInt()?{
????????int?i?=?Util.unsafe.getInt(address?+?position);
????????position?+=?4;
????????return?i;
????}
????public?int?position()?{
????????return?position;
????}
????public?void?position(int?position)?{
????????this.position?=?position;
????}
????public?void?limit(int?limit)?{
????????this.limit?=?limit;
????}
????public?void?flip()?{
????????limit?=?position;
????????position?=?0;
????}
????public?void?clear()?{
????????position?=?0;
????????limit?=?capacity;
????}
}
在一些比賽中,為了避免選手進(jìn)入無止境的內(nèi)卷,Unsafe 通常是禁用的,但是也有一些比賽,允許使用 Unsafe 的一部分能力,讓選手們放飛自我,探索可能性。例如 Unsafe#allocateMemory 是不會(huì)受到 -XX:MaxDirectMemory 和 -Xms 限制的,在這次第二屆云原生編程挑戰(zhàn)賽遭到了禁用,但 Unsafe#put 、Unsafe#get、Unsafe#copyMemory 允許被使用。如果你一定希望使用 Unsafe 操作堆外內(nèi)存,可以寫出這樣的代碼,它跟示例代碼一完成的是同樣的操作。
byte[]?src?=?...;
ByteBuffer?byteBuffer?=?ByteBuffer.allocateDirect(src.length);
long?address?=?((DirectBuffer)byteBuffer).address();
Util.unsafe.copyMemory(src,?16,?null,?address,?src.length);
這便是我想介紹的第一個(gè)關(guān)鍵點(diǎn):DirectByteBuffer 可以借助 Unsafe 完成內(nèi)存級(jí)別細(xì)粒度的操作,從而繞開邊界檢查。
DirectByteBuffer 的內(nèi)存初始化
注意到 DirectByteBuffer 構(gòu)造器中有另一個(gè)涉及到 Unsafe 的操作:unsafe.setMemory(base, size, (byte) 0);。這段代碼主要是為了給內(nèi)存初始化 0。說實(shí)話,我是沒有太懂這里的初始化操作,因?yàn)榘凑瘴业恼J(rèn)知,默認(rèn)值也是 0。在某些場(chǎng)景或者硬件下,內(nèi)存操作是非常昂貴的,尤其是大片的內(nèi)存被開辟時(shí),這段代碼可能會(huì)成為 DirectByteBuffer 的瓶頸。
如果希望分配內(nèi)存時(shí),不進(jìn)行這段初始化邏輯,可以借助于 Unsafe 分配內(nèi)存,再對(duì) DirectByteBuffer 進(jìn)行魔改。
public?class?AllocateDemo?{
????private?Field?addressField;
????private?Field?capacityField;
????
????public?AllocateDemo()?throws?NoSuchFieldException?{
????????Field?capacityField?=?Buffer.class.getDeclaredField("capacity");
????????capacityField.setAccessible(true);
????????Field?addressField?=?Buffer.class.getDeclaredField("address");
????????addressField.setAccessible(true);
????}
????
????public?ByteBuffer?allocateDirect(int?cap)?throws?IllegalAccessException?{
????????long?address?=?Util.unsafe.allocateMemory(cap);
????????ByteBuffer?byteBuffer?=?ByteBuffer.allocateDirect(1);
????????Util.unsafe.freeMemory(((DirectBuffer)?byteBuffer).address());
????????addressField.setLong(byteBuffer,?address);
????????capacityField.setInt(byteBuffer,?cap);
????????byteBuffer.clear();
????????return?byteBuffer;
????}
}
經(jīng)過這么一頓操作,我們便得到了一份沒有初始化的 DirectByteBuffer,不過不用擔(dān)心,一切都在正常工作,并且 setMemory for free!
聊聊 ByteBuffer 的零拷貝
算作是題外話了,主要是跟 ByteBuffer 相關(guān)的一個(gè)話題:零拷貝。ByteBuffer 在作為讀緩沖區(qū)時(shí)被使用時(shí),有一部分小伙伴會(huì)選擇使用加鎖的方式訪問內(nèi)存,但其實(shí)這是非常錯(cuò)誤的做法,應(yīng)當(dāng)使用 ByteBuffer 提供的 duplicate 和 slice 這兩個(gè)方法。
并發(fā)讀取緩沖的方案:
ByteBuffer?byteBuffer?=?ByteBuffer.allocateDirect(1024);
ByteBuffer?duplicate?=?byteBuffer.duplicate();
duplicate.limit(512);
duplicate.position(256);
ByteBuffer?slice?=?duplicate.slice();
//?use?slice
這樣便可以在不改變?cè)?ByteBuffer 指針的前提下,任意對(duì) slice 后的 ByteBuffer 進(jìn)行并發(fā)讀取了。
總結(jié)
最近時(shí)間有限,白天工作,晚上還要抽時(shí)間打比賽,先分享這么多。更多性能優(yōu)化小技巧,可以期待一下 1~2 個(gè)星期云原生比賽結(jié)束,我就開始繼續(xù)發(fā)總結(jié)和其他調(diào)優(yōu)方案。
本文閱讀求個(gè) 1000,不過分吧!
一鍵三連,這次一定。
