Presto源码分析之Slice

Slice是Presto里面用来对内存高效地、自由地进行操作的接口。它在Presto里面很关键, Presto里面另外一个关键类 Block 就大量用到了它,要充分理解 Block 首先就要先搞清楚 Slice , 今天就先来分析一下 Slice

Slice的结构

我们先来看看Slice的结构。Slice里面是通过三个参数来确定一个内存地址: base , address , size

  • base 是通过JVM分配出来的内存,在JVM层面是int数组、byte数组的对象,而对Slice来说这就是我们要操作的内存块
  • address 是我们要操作的地址离 base 这个对象头的偏移量.
  • size 是我们这块内存的大小,一般来说就是 base 底层所对应的内存的大小(in bytes), 或者更小一点。

我们来看看基于 byte[] 来创建一个Slice的构造函数就比较形象了:

    /**
     * Creates a slice over the specified array.
     */
    Slice(byte[] base)
    {
        requireNonNull(base, "base is null");
        this.base = base;
        this.address = ARRAY_BYTE_BASE_OFFSET;
        this.size = base.length;
        this.retainedSize = INSTANCE_SIZE + sizeOf(base);
        this.reference = COMPACT;
    }

其中 base 就是这个 byte数组, address 是一个来自 Unsafe 类里面常量: ARRAY_BYTE_BASE_OFFSET ,这个常量表示的是: byte数组里面第一个元素的地址离整个byte数组地址头的偏移量。为什么会有这么一个偏移量?因为数组不止有裸的byte数据,还有一些元数据在这些真正的数据之前,一个数组在JVM里面的元数据的结构如下:

数组的内存结构

由上图可见JVM数组的元数据有 class pointer , flags , locks , 以及一个数组的长度 size 一共是 128 位,也就是 16 个 byte, 而 ARRAY_BYTE_BASE_OFFSET 正好是 16, 意思是说从 byte[] 对象的首地址偏移 16 个字节才是真正开始保存数据的地方。

我们来看看从Slice里面获取一个Byte数据的方法 getByte 的实现:

    public byte getByte(int index)
    {
        checkIndexLength(index, SIZE_OF_BYTE);
        return getByteUnchecked(index);
    }

    byte getByteUnchecked(int index)
    {
        return unsafe.getByte(base, address + index);
    }

现在就比较好理解了,它是在获取byte数组从 address 开始算的第 index 个元素:

  • 如果 address == ARRAY_BYTE_BASE_OFFSET ,那么获取的就是第 index 个元素。
  • 如果 address > ARRAY_BYTE_BASE_OFFSET , 那么获取的元素的实际 indexindex + (address - ARRAY_BYTE_BASE_OFFSET)

同时,从代码细节我们还可以看到另外一点: 因为 Unsafe.getXxx 是完全不做任何检查的,Slice在调用之前还是做了边界检查的(checkIndexLength)。

Slice.slice()

Slice 里面还有一个很有意思的方法: Slice.slice():

    /**
     * Returns a slice of this buffer's sub-region. Modifying the content of
     * the returned buffer or this buffer affects each other's content.
     */
    public Slice slice(int index, int length)
    {
        if ((index == 0) && (length == length())) {
            return this;
        }
        checkIndexLength(index, length);
        if (length == 0) {
            return Slices.EMPTY_SLICE;
        }

        if (reference == COMPACT) {
            return new Slice(base, address + index, length, retainedSize, NOT_COMPACT);
        }
        return new Slice(base, address + index, length, retainedSize, reference);
    }

这个方法返回的是Slice里面从 index 开始的、长度为 length 的一段,有点数据库领域的 视图 的感觉。其实它的实现也给人 视图 的感觉, 从最后一行的 return 我们可以看出, 新的Slice底层对应的还是同样的 base 对象,只是 address 变成了 address + index , 这种通过 index / offset 来实现类似 视图 的做法在Presto代码里面很多地方都能看到。

unsignedByteToInt

Slice里面还有一个从 guava 里面拷贝过来的方法 unsignedByteToInt , 蛮有意思的,值得分析一下。

这个方法是一个私有方法,Slice用它来辅助实现 compareTo 方法,compareTo 方法是对Slice底层整个byte流进行比较,而 unsignedByteToInt 是用来对每一个 byte 进行比较,比较 naive 的想法是:

直接读出每一个byte然后对获取到的 byte 进行比较不就好了么?

这样实现是不对的,原因在于,java里面的 byte 是有符号数字: 用 8 个bit位表示的数字的范围是: -128 - 127, 从纯字节的角度来理解,一个8位都是1的byte( 1111_1111 )应该是最大的byte, 但是这个字节序列对应的Java里面的byte是: -1, 用 -1 代表 1111_1111 来比较大小结果当然不对,我们可以通过下面的Java代码来验证下:

public class ByteTest {
    public static void main(String[] args) {
        byte x = -1;                  // 1111_1111
        System.out.println(x & 0xFF); // 输出 255
        x = -128;                     // 1000_0000
        System.out.println(x & 0xFF); // 输出 128
        x = -127;                     // 1000_0001
        System.out.println(x & 0xFF); // 输出 129
        x = 0;                        // 0000_0000
        System.out.println(x & 0xFF); // 输出 0
    }
}

这里也可以顺便总结一下Java里面 byte 的内存表示:

  • 第一位是符号位, 1 表示负数。
  • byte 最小值 -128 符号为 1 , 其它位是 0: 1000_0000
  • 从最小值往上数,其它位的数值开始递增,比如 -127的内存表示: 1000_0001

其它数字类型的内存表示也是类似的,比如 Long.MIN_VALUE 的内存表示是:
10000000_00000000_00000000_00000000_00000000_00000000_00000000_00000000

因此通过 byte & 0xFF 把这个有符号的byte, 变成一个int, 才能对byte的数据流进行正确的大小比较:

    private static int unsignedByteToInt(byte thisByte)
    {
        return thisByte & 0xFF;
    }

总结

Slice 在 sun.misc.Unsafe 之上封装了一个简单、好用的Java层面可以对内存进行自由操作的接口。你可以通过Slice接口来获取指定地址的Int , Short , Byte , 同样也可以对指定区域的内存的值进行设置。

为什么不直接使用 Unsafe 呢? 我理解可能有两方面的原因:一是因为Unsafe的首要目的是快,因此它的接口是不安全的,接口都不做越界检查,同时就比较难用,Slice通过包装一层,使得接口更易用;另一方面,通过对Unsafe接口进行一层代理,使得主要的核心代码不依赖 Unsafe 这个其实不是那么可靠(Unsafe本身并不是一个公开API,理论上来说在以后的版本是可以干掉的)的接口。

参考资料

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,236评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,867评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,715评论 0 340
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,899评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,895评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,733评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,085评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,722评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,025评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,696评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,816评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,447评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,057评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,009评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,254评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,204评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,561评论 2 343

推荐阅读更多精彩内容