Arrays.equals与ArraysSupport.vectorizedMismatch方法

博客发表于:Ghamster Blog
转载请注明出处

概述

  • Arrays类是Java中用于数组操作的工具类,实现了比较、复制、查找和排序等常用操作
  • equals方法用于判断数组是否相等,包含了各种基本类型和Object类型的重载版本,对于Object[]会判断对象非空,并调用Object.equals判断对象是否相等
  • ArraysSupport类是jdk11(似乎是在jdk9引入)的工具类,提供了mismatch方法和vectorizedMismatchmismatch方法可以返回两个被判定的数组对象中,首个不相等元素的下标
  • jdk11中使用mismatch方法修改了equals方法的实现逻辑

Arrays类的equals方法实现

jdk8中,数组判等的逻辑是:遍历数组中的元素,依次对每个元素判等
int[]为例,源码如下:

    public static boolean equals(int[] a, int[] a2) {
        if (a==a2)
            return true;
        if (a==null || a2==null)
            return false;

        int length = a.length;
        if (a2.length != length)
            return false;

        for (int i=0; i<length; i++)
            if (a[i] != a2[i])
                return false;

        return true;
    }

jdk11中,修改了所有用于基本数据类型的数组的equals方法,使用mismatch方法实现
int[]为例,源码如下:

    public static boolean equals(int[] a, int[] a2) {
        if (a==a2)
            return true;
        if (a==null || a2==null)
            return false;

        int length = a.length;
        if (a2.length != length)
            return false;

        return ArraysSupport.mismatch(a, a2, length) < 0;
    }

mismatch和vectorizedMismatch

为了探究jdk11equals的魔法,必须看看mismatch方法到底做了什么。mismatch方法也拥有各种基本数据类型数组的重载版本,以int[]版本为例

mismatch方法

mismatch方法返回给定的两个数组中,首个不相同元素的下标,当完全匹配(0到length-1)时,返回-1,源码如下:

    public static int mismatch(int[] a,
                               int[] b,
                               int length) {
        int i = 0;
        if (length > 1) {
            if (a[0] != b[0])
                return 0;
            i = vectorizedMismatch(
                    a, Unsafe.ARRAY_INT_BASE_OFFSET,
                    b, Unsafe.ARRAY_INT_BASE_OFFSET,
                    length, LOG2_ARRAY_INT_INDEX_SCALE);
            if (i >= 0)
                return i;
            i = length - ~i;
        }
        for (; i < length; i++) {
            if (a[i] != b[i])
                return i;
        }
        return -1;
    }

方法主要可以分为两个部分:

  1. 调用vectorizedMismatch方法,对两个数组对象匹配,若方法返回正值,则表示找到了未匹配的元素,返回值为该元素在数组中的索引;若方法返回负值,则表示未找到不相等元素,但需要注意的是,vectorizedMismatch方法可能没有处理完数组中的所有元素,对返回值取反,得到还需处理的数组元素个数
  2. vectorizedMismatch返回值取反,对剩余元素逐个验证

vectorizedMismatch

该方法标注了 @HotSpotIntrinsicCandidate 注解,即jvm可以有自己的优化实现方式,所以...懂吧...?

该方法的核心思想是以long数据格式对比对象(不一定是数组)中的数据,即每次循环处理数据长度为8个字节,而不考虑对象是long[]还是int[]short[]等。方法的源码如下:

@HotSpotIntrinsicCandidate
    public static int vectorizedMismatch(Object a, long aOffset,
                                         Object b, long bOffset,
                                         int length,
                                         int log2ArrayIndexScale) {
        // assert a.getClass().isArray();
        // assert b.getClass().isArray();
        // assert 0 <= length <= sizeOf(a)
        // assert 0 <= length <= sizeOf(b)
        // assert 0 <= log2ArrayIndexScale <= 3

        int log2ValuesPerWidth = LOG2_ARRAY_LONG_INDEX_SCALE - log2ArrayIndexScale;
        int wi = 0;
        for (; wi < length >> log2ValuesPerWidth; wi++) {
            long bi = ((long) wi) << LOG2_ARRAY_LONG_INDEX_SCALE;
            long av = U.getLongUnaligned(a, aOffset + bi);
            long bv = U.getLongUnaligned(b, bOffset + bi);
            if (av != bv) {
                long x = av ^ bv;
                int o = BIG_ENDIAN
                        ? Long.numberOfLeadingZeros(x) >> (LOG2_BYTE_BIT_SIZE + log2ArrayIndexScale)
                        : Long.numberOfTrailingZeros(x) >> (LOG2_BYTE_BIT_SIZE + log2ArrayIndexScale);
                return (wi << log2ValuesPerWidth) + o;
            }
        }

        // Calculate the tail of remaining elements to check
        int tail = length - (wi << log2ValuesPerWidth);

        if (log2ArrayIndexScale < LOG2_ARRAY_INT_INDEX_SCALE) {
            int wordTail = 1 << (LOG2_ARRAY_INT_INDEX_SCALE - log2ArrayIndexScale);
            // Handle 4 bytes or 2 chars in the tail using int width
            if (tail >= wordTail) {
                long bi = ((long) wi) << LOG2_ARRAY_LONG_INDEX_SCALE;
                int av = U.getIntUnaligned(a, aOffset + bi);
                int bv = U.getIntUnaligned(b, bOffset + bi);
                if (av != bv) {
                    int x = av ^ bv;
                    int o = BIG_ENDIAN
                            ? Integer.numberOfLeadingZeros(x) >> (LOG2_BYTE_BIT_SIZE + log2ArrayIndexScale)
                            : Integer.numberOfTrailingZeros(x) >> (LOG2_BYTE_BIT_SIZE + log2ArrayIndexScale);
                    return (wi << log2ValuesPerWidth) + o;
                }
                tail -= wordTail;
            }
            return ~tail;
        }
        else {
            return ~tail;
        }
    }

首先简单看一下方法的入参:

  • Object a, Object b:需要对比的对象
  • long aOffset, long bOffset:起始Offset,即从对象的第几个字节开始对比。比如本例中,被mismatch方法调用时,传入的参数为Unsafe.ARRAY_INT_BASE_OFFSET,通过debug发现在当前平台上该值为16,因为从第16字节开始,才是数组中的数据(0-15字节存储的应该是数组的length等信息)
  • length:需要对比的长度。该方法并不提供数组下标越界检查,因此length必须是合理的数值
  • log2ArrayIndexScale:数组下标缩放,与length共同确定需要对比的字节长度。以int[]为例,int型数据占4个字节,即1<<2,所以log2ArrayIndexScale为2,方法需要处理的字节长度为length*(1<<2)=4*length

方法操作逻辑如下(逐行;接上节,以int[]为例):

  • 12行: log2ValuesPerWidth可以理解为相对缩放。LOG2_ARRAY_LONG_INDEX_SCALE长整型数组下标缩放为3,即长8字节;int[]缩放为2,即长4字节;所以相对缩放为1
  • 13行:wi,将对象视为long[]遍历时,当前数组下标
  • 14行: for循环,每次wi++,即每次循环步进8个字节。length为数组包含的int数据个数,将素组视为long时,循环length>>log2ArrayIndexScale即可遍历数组
  • 15行: bi数组下标为wi的元素在long数组中的相对位置(字节)
  • 16-17行: 调用UnSafe.getLongUnaligned方法,读取一个long值。
    在Java中,数据是对齐存放的,即long类型的数据存放时,起始地址只能是8的整数倍,int类型数据存放的起始地址只能是4的整数倍……
    getLongUnaligned方法顾名思义,可以读取非对齐存放的数据。该方法接收一个Object对象和一个long类型的offset:若offset为8的整数倍,即(offset & 7) == 0,则直接调用getLong返回长整型数据;若offset为4的整数倍,则直接分别对offsetoffset+4调用getInt返回两个整型数据,然后通过<<4&操作,将两个int型数据合并成一个long;若offset为2的整数倍……
  • 18-24行: 判定两个数组的值av==bv是否成立,相同则继续下一次循环;不同是使用^按位与或,并根据系统采用大端存储还是小端存储决定从头(numberOfLeadingZeros)还是尾(numberOfTrailingZeros)找到第一个不匹配的位。然后根据log2ArrayIndexScale的值,还原这个位在原数组int[]中对应的索引。LOG2_BYTE_BIT_SIZE值为3,表示一个字节共8位
  • 27-50行: 用于处理数组类型为byte[]char[],且剩余未处理长度大于4字节(一个int)的情况,原理与前面类似。
    tail表示剩余未处理的元素个数,由于UnSafe.getIntUnaligned只能一次处理4个字节,因此可能剩余1-3个byte或1个char

性能测试

下面通过简单代码测试一下相对于jdk8jdk11是否有性能的提升,测试代码如下:

package main.test;

import java.util.Arrays;
import java.util.Random;

public class TestLegacyArrays {

    private static Random r = new Random();

    private static class LegacyArrays {
        private LegacyArrays() {}

        public static boolean equals(int[] a, int[] a2) {
            if (a==a2)
                return true;
            if (a==null || a2==null)
                return false;

            int length = a.length;
            if (a2.length != length)
                return false;

            for (int i=0; i<length; i++)
                if (a[i] != a2[i])
                    return false;

            return true;
        }
    }

    private static void test(int size, int fill){
        int[] a = new int[size];
        Arrays.fill(a, fill);
        int[] b = new int[size];
        Arrays.fill(b, fill);

        boolean isEqual;
        long now, finish;
        System.out.println("=> TestLegacyArrays: array size "+size);

        now = System.nanoTime();
        isEqual = LegacyArrays.equals(a,b);
        finish = System.nanoTime();
        System.out.println("  LegacyArrays: "+isEqual+" time: "+(finish-now));

        now = System.nanoTime();
        isEqual = Arrays.equals(a,b);
        finish = System.nanoTime();
        System.out.println("  Arrays: "+isEqual+" time: "+(finish-now));
        System.out.println();
    }

    public static void main(String[] args) {
        
        test(10, r.nextInt());
        test(100, r.nextInt());
        test(1000, r.nextInt());
        test(10000, r.nextInt());
        test(100000, r.nextInt());

    }
}

代码输出如下:

=> TestLegacyArrays: array size 10
  LegacyArrays: true time: 373700
  Arrays: true time: 193700

=> TestLegacyArrays: array size 100
  LegacyArrays: true time: 2000
  Arrays: true time: 10200

=> TestLegacyArrays: array size 1000
  LegacyArrays: true time: 15400
  Arrays: true time: 186500

=> TestLegacyArrays: array size 10000
  LegacyArrays: true time: 148900
  Arrays: true time: 286800

=> TestLegacyArrays: array size 100000
  LegacyArrays: true time: 1233700
  Arrays: true time: 2424000


Process finished with exit code 0

emmmmmmmmmmmmmmm...................
所以这次的更新,或许是出于流式计算或者多线程或者其他因素的考量,又或者我的测试方法有问题?。。。

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

推荐阅读更多精彩内容

  • # Java NIO # Java NIO属于非阻塞IO,这是与传统IO最本质的区别。传统IO包括socket和文...
    Teddy_b阅读 581评论 0 0
  • 一、基础知识:1、JVM、JRE和JDK的区别:JVM(Java Virtual Machine):java虚拟机...
    杀小贼阅读 2,362评论 0 4
  • Lua 5.1 参考手册 by Roberto Ierusalimschy, Luiz Henrique de F...
    苏黎九歌阅读 13,714评论 0 38
  • 前提 参考资料: 《Java I/O》 -- 这本书没有翻译版,需要自己啃一下。 《Java I/O》这本书主要介...
    zhrowable阅读 1,153评论 0 1
  • 2018年3月8日晴,今天上班要走的时候,告诉儿子要把今天的作业完成。儿子答应的很快,孩子的爸爸说哎呀快走啦,放心...
    冰雪_369e阅读 277评论 0 0