被识破了!如何使用sun.misc.Unsafe加速byte[]查找?

8

我正在尝试使用Unsafe来遍历内存,而不是遍历byte[]数组中的值。使用unsafe分配了一个内存块,该内存足以容纳65536个字节值。

我正在尝试以下内容:

char aChar = some character

if ((byte) 0 == (unsafe.getByte(base_address + aChar) & mask)){
 // do something
}

改为:

char aChar = some character

if ((byte) 0 == ( lookup[aChar] & mask )){
 // do something
}

我以为Unsafe能够比使用常规数组访问更快,因为它对每个索引进行了索引检查...但这只是一厢情愿的想法,我认为jvm不会有一个特殊的操作(unsafe)可以使常规数组访问和迭代更快。在我的看来,jvm使用正常的byte[]迭代工作得很好,并且使用正常的、未经改变的、纯净的Java代码,速度非常快。@millimoose 点出了问题所在:'Unsafe可能对许多事情有用,但这种微观优化并不是其中之一。'在极其严格的有限情况下,使用Unsafe会更快:(64位jvm only)对于每个测试仅执行一次的单个65535 byte[]查找更快。在这种情况下,64位jvm上的UnsafeLookup_8B比普通方法快24%。如果测试重复,使每个测试都执行两次,则正常方法现在比unsafe快30%。在冷启动的纯解释模式下,Unsafe要快得多——但仅限于第一次和小数组大小。在32位标准Oracle JVM 7.x上,正常方法比使用unsafe快三倍。在我的测试中,使用Unsafe更慢:无论是在Oracle Java 64位还是32位虚拟机上,也无论是在哪种操作系统和机器架构(32位和64位),使用Unsafe都更慢。即使调用了serverjvm选项,它也会更慢。Unsafe在32位jvm上比正常方法慢9%或更多(下面代码中1_GB数组和UnsafeLookup_8B(最快的)),在64位jvm上甚至更慢??Unsafe在64位jvm上比正常方法慢234%或更多(下面代码中1_MB数组和UnsafeLookup_1B(最快的))。这是为什么呢?当我运行yellowB发布的代码(检查一个1GB byte[])时,正常方法仍然是最快的。
C:\Users\wilf>java -Xms1600m -Xprof -jar "S:\wilf\testing\dist\testing.jar"
initialize data...
initialize data done!

use normalLookup()...
Not found '0'
time : 1967737 us.

use unsafeLookup_1B()...
Not found '0'
time : 2923367 us.

use unsafeLookup_8B()...
Not found '0'
time : 2495663 us.

Flat profile of 26.35 secs (2018 total ticks): main

  Interpreted + native   Method
  0.0%     1  +     0    test.StackOverflow.main
  0.0%     1  +     0    Total interpreted

     Compiled + native   Method
 67.8%  1369  +     0    test.StackOverflow.main
 11.7%   236  +     0    test.StackOverflow.unsafeLookup_8B
 11.2%   227  +     0    test.StackOverflow.unsafeLookup_1B
  9.1%   184  +     0    test.StackOverflow.normalLookup
 99.9%  2016  +     0    Total compiled

         Stub + native   Method
  0.0%     0  +     1    sun.misc.Unsafe.getLong
  0.0%     0  +     1    Total stub


Flat profile of 0.00 secs (1 total ticks): DestroyJavaVM

  Thread-local ticks:
100.0%     1             Blocked (of total)


Global summary of 26.39 seconds:
100.0%  2023             Received ticks


C:\Users\wilf>java -version
java version "1.7.0_07"
Java(TM) SE Runtime Environment (build 1.7.0_07-b11)
Java HotSpot(TM) Client VM (build 23.3-b01, mixed mode, sharing)

CPU是:Intel Core 2 Duo E4600 @ 2.4GHZ,内存大小为4.00GB(其中3.25GB可用)。

操作系统为:Windows 7(32位)。

在运行测试时,使用的是带有Windows 7_64操作系统和32位Java的4核AMD64处理器:

initialize data...
initialize data done!

use normalLookup()...
Not found '0'
time : 1631142 us.

use unsafeLookup_1B()...
Not found '0'
time : 2365214 us.

use unsafeLookup_8B()...
Not found '0'
time : 1783320 us.

在一台配备 Windows 7_64 操作系统、4核 AMD64 处理器以及64位 Java 环境的计算机上运行测试:
use normalLookup()...
Not found '0'
time : 655146 us.

use unsafeLookup_1B()...
Not found '0'
time : 904783 us.

use unsafeLookup_8B()...
Not found '0'
time : 764427 us.

Flat profile of 6.34 secs (13 total ticks): main

  Interpreted + native   Method
 23.1%     3  +     0    java.io.PrintStream.println
 23.1%     3  +     0    test.StackOverflow.unsafeLookup_8B
 15.4%     2  +     0    test.StackOverflow.main
  7.7%     1  +     0    java.io.DataInputStream.<init>
 69.2%     9  +     0    Total interpreted

     Compiled + native   Method
  7.7%     0  +     1    test.StackOverflow.unsafeLookup_1B
  7.7%     0  +     1    test.StackOverflow.main
  7.7%     0  +     1    test.StackOverflow.normalLookup
  7.7%     0  +     1    test.StackOverflow.unsafeLookup_8B
 30.8%     0  +     4    Total compiled


Flat profile of 0.00 secs (1 total ticks): DestroyJavaVM

  Thread-local ticks:
100.0%     1             Blocked (of total)


Global summary of 6.35 seconds:
100.0%    14             Received ticks
 42.9%     6             Compilation

2
你可以尝试使用Xprof标志,并将输出粘贴到这里(java -Xprof SampleClass > output.txt)。 - jknair
2
你需要提供基准测试的代码。很可能你没有为JVM做足够的预热,以便对Unsafe版本进行JIT编译。然而,从提供的代码来看,我认为不会有任何加速。只有在读取比一个字节更宽的类型时,例如,你想读取一个单独的长整型而不是8个单独的字节,并应用添加和移位操作来获取所需的值时,Unsafe才会提供优势。 - Michael Barker
你期望方法调用比简单的数组查找更快。我认为除非它被内联,否则任何方法调用都比数组查找慢得多,因此您可能没有触发内联。 - millimoose
1
@Wilf 如果我理解你的测试用例正确的话,你仍在将数据从内存的一部分移动到另一部分与其他移动数据的方式进行比较。这似乎不是该类首先要优化的内容。请注意,所有的 Unsafe 方法都被声明为 native。调用本地方法可能涉及到 JVM 无法优化的一些开销。如果您愿意首先使用本地代码来优化此类事情,最好尽可能避免跨越本地-JVM 边界。 - millimoose
1
从搜索结果来看,我得出的结论是“不太常见”。Unsafe 可能对很多事情有用,但这种微观优化并不是其中之一。 - millimoose
显示剩余5条评论
3个回答

5
我认为你发布的两个函数基本上是相同的,因为它们只读取一个字节,然后将其转换为int并进行进一步比较。
每次读取4字节int或8字节long会更有效。我编写了两个函数来做同样的事情:比较两个byte []的内容是否相同:
函数1:
public static boolean hadoopEquals(byte[] b1, byte[] b2)
  {
    if(b1 == b2)
    {
      return true;
    }
    if(b1.length != b2.length)
    {
      return false;
    }
    // Bring WritableComparator code local

    for(int i = 0;i < b1.length; ++i)
    {
     int a = (b1[i] & 0xff);
     int b = (b2[i] & 0xff);
     if (a != b) 
     {
       return false;
     }
    }
    return true;
  }

功能 2:

public static boolean goodEquals(byte[] b1,byte[] b2)
  {   
    if(b1 == b2)
    {
      return true;
    }
    if(b1.length != b2.length)
    {
      return false;
    }
    int baseOffset = UnSafe.arrayBaseOffset(byte[].class);

    int numLongs = (int)Math.ceil(b1.length / 8.0);

    for(int i = 0;i < numLongs; ++i)
    {
      long currentOffset = baseOffset + (i * 8);
      long l1 = UnSafe.getLong(b1, currentOffset);
      long l2 = UnSafe.getLong(b2, currentOffset);
      if(0L != (l1 ^ l2))
      {
        return false;
      }
    }
    return true;    
  }

我在我的笔记本电脑上(corei7 2630QM , 8GB DDR3 , 64位 win 7 , 64位 Hotspot JVM)运行了这两个函数,并比较了两个400MB的byte[],结果如下:

函数1:约670毫秒

函数2:约80毫秒

函数2更快。

因此,我的建议是每次读取8个字节并使用异或运算符(^):

long l1 = UnSafe.getLong(byteArray, offset);  //8 byte
if(0L == l1 ^ 0xFF)  //if the lowest byte == 0?
/* do something */
if(0L == l1 ^ 0xFF00)  //if the 2nd lowest byte == 0?
/* do something */
/* go on... */

============================================================================

嗨,Wilf, 我使用你的代码创建了一个测试类,如下所示。这个类比较了在字节数组中查找第一个0时3个函数的速度:

package test;

import java.lang.reflect.Field;

import sun.misc.Unsafe;

/**
 * Test the speed in looking up the 1st 0 in a byte array
 * Set -Xms the same as -Xms to avoid Heap reallocation
 * 
 * @author yellowb
 *
 */
public class StackOverflow
{
    public static Unsafe UnSafe;

    public static Unsafe getUnsafe() throws SecurityException,
            NoSuchFieldException, IllegalArgumentException,
            IllegalAccessException
    {
        Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
        theUnsafe.setAccessible(true);
        Unsafe unsafe = (Unsafe) theUnsafe.get(null);
        return unsafe;
    }

    /**
     * use 'byte[index]' form to read 1 byte every time
     * @param buf
     */
    public static void normalLookup(byte[] buf)
    {
        for (int i = 0; i < buf.length; ++i)
        {
            if ((byte) 0 == buf[i])
            {
                System.out.println("The 1st '0' is at position : " + i);
                return;
            }
        }
        System.out.println("Not found '0'");
    }

    /**
     * use Unsafe.getByte to read 1 byte every time directly from the memory
     * @param buf
     */
    public static void unsafeLookup_1B(byte[] buf)
    {
        int baseOffset = UnSafe.arrayBaseOffset(byte[].class);
        for (int i = 0; i < buf.length; ++i)
        {
            byte b = UnSafe.getByte(buf, (long) (baseOffset + i));
            if (0 == ((int) b & 0xFF))
            {
                System.out.println("The 1st '0' is at position : " + i);
                return;
            }

        }
        System.out.println("Not found '0'");
    }

    /**
     * use Unsafe.getLong to read 8 byte every time directly from the memory
     * @param buf
     */
    public static void unsafeLookup_8B(byte[] buf)
    {
        int baseOffset = UnSafe.arrayBaseOffset(byte[].class);

        //The first (numLongs * 8) bytes will be read by Unsafe.getLong in below loop
        int numLongs = buf.length / 8;
        long currentOffset = 0L;
        for (int i = 0; i < numLongs; ++i)
        {
            currentOffset = baseOffset + (i * 8);  //the step is 8 bytes
            long l = UnSafe.getLong(buf, currentOffset);
            //Compare each byte(in the 8-Byte long) to 0
            //PS:x86 cpu is little-endian mode
            if (0L == (l & 0xFF))
            {
                System.out.println("The 1st '0' is at position : " + (i * 8));
                return;
            }
            if (0L == (l & 0xFF00L))
            {
                System.out.println("The 1st '0' is at position : " + (i * 8 + 1));
                return;
            }
            if (0L == (l & 0xFF0000L))
            {
                System.out.println("The 1st '0' is at position : " + (i * 8 + 2));
                return;
            }
            if (0L == (l & 0xFF000000L))
            {
                System.out.println("The 1st '0' is at position : " + (i * 8 + 3));
                return;
            }
            if (0L == (l & 0xFF00000000L))
            {
                System.out.println("The 1st '0' is at position : " + (i * 8 + 4));
                return;
            }
            if (0L == (l & 0xFF0000000000L))
            {
                System.out.println("The 1st '0' is at position : " + (i * 8 + 5));
                return;
            }
            if (0L == (l & 0xFF000000000000L))
            {
                System.out.println("The 1st '0' is at position : " + (i * 8 + 6));
                return;
            }
            if (0L == (l & 0xFF00000000000000L))
            {
                System.out.println("The 1st '0' is at position : " + (i * 8 + 7));
                return;
            }
        }

        //If some rest bytes exists
        int rest = buf.length % 8;
        if(0 != rest)
        {
            currentOffset = currentOffset + 8;
            //Because the length of rest bytes < 8,we have to read them one by one
            for(; currentOffset < (baseOffset + buf.length); ++currentOffset)
            {
                byte b = UnSafe.getByte(buf, (long)currentOffset);
                if (0 == ((int) b & 0xFF))
                {
                    System.out.println("The 1st '0' is at position : " + (currentOffset - baseOffset));
                    return;
                }
            }
        }
        System.out.println("Not found '0'");
    }

    public static void main(String[] args) throws SecurityException,
            NoSuchFieldException, IllegalArgumentException,
            IllegalAccessException
    {
        UnSafe = getUnsafe();

        int len = 1024 * 1024 * 1024;  //1G
        long startTime = 0L;
        long endTime = 0L;

        System.out.println("initialize data...");
        byte[] byteArray1 = new byte[len];
        for (int i = 0; i < len; ++i)
        {
            byteArray1[i] = (byte) (i % 128 + 1);  //No byte will equal to 0
        }
        //If you want to set one byte to 0,uncomment the below statement
//      byteArray1[2500] = (byte)0;
        System.out.println("initialize data done!");

        System.out.println("use normalLookup()...");
        startTime = System.nanoTime();
        normalLookup(byteArray1);
        endTime = System.nanoTime();
        System.out.println("time : " + ((endTime - startTime) / 1000) + " us.");

        System.out.println("use unsafeLookup_1B()...");
        startTime = System.nanoTime();
        unsafeLookup_1B(byteArray1);
        endTime = System.nanoTime();
        System.out.println("time : " + ((endTime - startTime) / 1000) + " us.");

        System.out.println("use unsafeLookup_8B()...");
        startTime = System.nanoTime();
        unsafeLookup_8B(byteArray1);
        endTime = System.nanoTime();
        System.out.println("time : " + ((endTime - startTime) / 1000) + " us.");
    }
}

输出结果为:

initialize data...
initialize data done!
use normalLookup()...
Not found '0'
time : 1271781 us.
use unsafeLookup_1B()...
Not found '0'
time : 716898 us.
use unsafeLookup_8B()...
Not found '0'
time : 591689 us.

结果显示,即使通过Unsafe.getByte()每次只读取1个字节,速度也比常规迭代byte[]要快得多。读取8个字节长的数据最快。


非常有趣。我会看看我的基准测试显示什么。感谢您提供的示例代码和解释。 - The Coordinator
我在32位win7 + jRockit + intel Pentium E5300 + 4G上运行了测试,结果是:normalLookup() = 4628773微秒,unsafeLookup_1B() = 5653817微秒,unsafeLookup_8B() = 1638572微秒。看起来在32位旧CPU上,unsafeLookup_1B()比normalLookup()慢? - yellowB
我重新运行并重复了每个测试50次(这意味着每种方法都运行了50遍)。出乎意料的是,正常方法现在是最快的。normalLookup = 29,492,009微秒,unsafeLookup_1B = 43,727,754微秒,unsafeLookup_8B = 37,804,323微秒。所以,似乎在某一点上jvm会对正常方法进行优化,并且它运行得最快。 - The Coordinator
根据我的测试,使用大量的数组大小和不同的迭代,似乎正常方法仍然是最快的。 - The Coordinator
请列出您的硬件和软件详细信息(CPU/内存/操作系统...)。在我的情况下,unsafeLookup_8B是最快的,无论是在32位模式还是64位模式下。 - yellowB
CPU:Intel Core 2 Duo E4600 @ 2.4GHZ 4.00GB(3.25GB可用) 操作系统:Windows 7(32位) JVM:Java(TM) SE Runtime Environment(版本1.7.0_07-b11) Java HotSpot(TM) Client VM(版本23.3-b01,混合模式,共享) - The Coordinator

1
我认为使用Unsafe可以比使用常规数组访问更快地访问内存,因为它不需要为每个索引进行索引检查...
可能的一个原因是JIT编译器的优化器可能不会考虑范围检查。由于数组的大小永远不会改变,因此优化器可能会将所有的范围检查“提升”并在循环开始时执行一次。
相比之下,JIT编译器可能无法优化(例如内联)Unsafe.getByte()调用。或者也许getByte方法有一个读取屏障...)
然而这只是猜测。确定的方法是让JVM转储两种情况的JIT编译本机代码,并逐条比较它们。

有人能做到这个吗?在Windows上的常规jvm上是否可能实现? - The Coordinator
1
是的,在Windows上可以做到这一点。https://wikis.oracle.com/display/HotSpotInternals/PrintAssembly - Stephen C
我在哪里可以找到适用于Windows X86或64的最近版本的OpenJDK7? - The Coordinator
Java 7 JDK从Oracle网站下载应该没问题。你可以在Oracle首页找到它。 - Stephen C
你需要获取描述本地代码指令集的英特尔(可能)手册,然后使用该手册:1)确定相关代码部分的位置,2)阅读并比较它们,3)分析理论执行时间。这很困难...尤其是如果你以前从未阅读过本地代码。 - Stephen C
在我做那件事之前,我宁愿接受开放性心脏手术! - The Coordinator

0

不安全的方法可能被标记为本地方法,但这并不意味着它们一定是JNI。几乎所有的不安全方法都是内置函数(请参见此处的短帖子:http://psy-lob-saw.blogspot.co.uk/2012/10/java-intrinsics-are-not-jni-calls.html),对于Sun JVM,它们将被转换为单个汇编指令(在许多情况下),对于其他JVM,它们可能会或可能不会处理内置函数,并将其转换为JNI调用或普通Java调用。据我所知,JRockit倾向于采用JNI方式,Android JVM也是如此。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接