这个Java代码如何加速?

时间:2011-09-16 23:27:57

标签: java optimization benchmarking compiler-optimization micro-optimization

我正在尝试对Java执行简单任务的速度进行基准测试:将大文件读入内存,然后对数据执行一些无意义的计算。所有类型的优化都很重要。无论是以不同方式重写代码还是使用不同的JVM,都会欺骗JIT ..

输入文件是一个由逗号分隔的5亿个32位整数对列表。像这样:

  

44439,5023
  33140,22257
  ...

此文件在我的计算机上占用 5.5GB 。该程序不能使用超过 8GB 的RAM,只能使用单线程

package speedracer;

import java.io.FileInputStream;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;

public class Main
{
    public static void main(String[] args)
    {
        int[] list = new int[1000000000];

        long start1 = System.nanoTime();
        parse(list);
        long end1 = System.nanoTime();

        System.out.println("Parsing took: " + (end1 - start1) / 1000000000.0);

        int rs = 0;
        long start2 = System.nanoTime();

        for (int k = 0; k < list.length; k++) {
            rs = calc(list[k++], list[k++], list[k++], list[k]);
        }

        long end2 = System.nanoTime();

        System.out.println(rs);
        System.out.println("Calculations took: " + (end2 - start2) / 1000000000.0);
    }

    public static int calc(final int a1, final int a2, final int b1, final int b2)
    {
        int c1 = (a1 + a2) ^ a2;
        int c2 = (b1 - b2) << 4;

        for (int z = 0; z < 100; z++) {
            c1 ^= z + c2;
        }

        return c1;
    }

    public static void parse(int[] list)
    {
        FileChannel fc = null;
        int i = 0;

        MappedByteBuffer byteBuffer;

        try {
            fc = new FileInputStream("in.txt").getChannel();

            long size = fc.size();
            long allocated = 0;
            long allocate = 0;

            while (size > allocated) {

               if ((size - allocated) > Integer.MAX_VALUE) {
                   allocate = Integer.MAX_VALUE;
               } else {
                   allocate = size - allocated;
               }

               byteBuffer = fc.map(FileChannel.MapMode.READ_ONLY, allocated, allocate);
               byteBuffer.clear();

               allocated += allocate;

               int number = 0;

               while (byteBuffer.hasRemaining()) {
                   char val = (char) byteBuffer.get();
                   if (val == '\n' || val == ',') {
                        list[i] = number;

                        number = 0;
                        i++;
                   } else {
                       number = number * 10 + (val - '0');
                   }
                }
            }

            fc.close();

        } catch (Exception e) {
            System.err.println("Parsing error: " + e);
        }
    }
}

我已经尝试了所有我能想到的。尝试不同的读者,尝试openjdk6,sunjdk6,sunjdk7。试过不同的读者。不得不做一些丑陋的解析,因为MappedByteBuffer不能同时映射超过2GB的内存。我正在跑步:

   Linux AS292 2.6.38-11-generic #48-Ubuntu SMP 
   Fri Jul 29 19:02:55 UTC 2011 
   x86_64 GNU/Linux. Ubuntu 11.04. 
   CPU: is Intel(R) Core(TM) i5-2410M CPU @ 2.30GHz.

目前,我的结果是解析:26.50s,计算:11.27s。我正在与类似的C ++基准测试竞争,后者几乎在同一时间执行IO,但计算只需4.5秒。我的主要目标是以任何可能的方式减少计算时间。有什么想法吗?

更新:似乎主要的速度提升可能来自所谓的Auto-Vectorization。我能够找到一些暗示当前Sun的JIT只做“一些矢量化”,但我无法确认它。找到一些能够提供更好的自动矢量化优化支持的JVM或JIT会很棒。

7 个答案:

答案 0 :(得分:4)

首先,-O3启用:

-finline-functions
-ftree-vectorize

等等......

所以看起来它实际上可能是矢量化。

编辑: 这已得到确认。 (参见注释) C ++版本确实被编译器矢量化了。禁用矢量化后,C ++版本的运行速度实际上比Java版本慢一些

假设JIT没有对循环进行矢量化,可能难以/不可能使Java版本与C ++版本的速度相匹配。


现在,如果我是一个聪明的C / C ++编译器,这就是我如何安排该循环(在x64上):

int c1 = (a1 + a2) ^ a2;
int c2 = (b1 - b2) << 4;

int tmp0 = c1;
int tmp1 = 0;
int tmp2 = 0;
int tmp3 = 0;

int z0 = 0;
int z1 = 1;
int z2 = 2;
int z3 = 3;

do{
    tmp0 ^= z0 + c2;
    tmp1 ^= z1 + c2;
    tmp2 ^= z2 + c2;
    tmp3 ^= z3 + c2;
    z0 += 4;
    z1 += 4;
    z2 += 4;
    z3 += 4;
}while (z0 < 100);

tmp0 ^= tmp1;
tmp2 ^= tmp3;

tmp0 ^= tmp2;

return tmp0;

请注意,此循环完全可以向量化。

更好的是,我会完全展开这个循环。这些是C / C ++编译器将要做的事情。但现在问题是,JIT会这样做吗?

答案 1 :(得分:1)

在服务器模式下使用Hotspot JVM,并确保warm it up。如果集合是测试的主要部分,还要给垃圾收集算法留出足够的时间以稳定速度。我一眼就看不到任何让我觉得会......的事情......

答案 2 :(得分:1)

有趣的问题。 :-)这可能更多的是评论,因为我不会真正回答你的问题,但评论框太长了。

Java中的微基准测试非常棘手,因为JIT可以通过优化来解决问题。但是这个特殊的代码以某种方式欺骗JIT,使得它无法以某种方式执行其正常的优化。

通常,此代码将在O(1)时间内运行,因为您的主循环对任何内容都没有影响:

    for (int k = 0; k < list.length; k++) {
        rs = calc(list[k++], list[k++], list[k++], list[k]);
    }

请注意,rs的最终结果并不真正依赖于运行循环的所有迭代;只是最后一个。您可以计算循环的“k”的最终值,而无需实际运行循环。通常情况下,JIT会注意到并将您的循环转换为单个赋值,它能够检测到被调用的函数(calc)没有副作用(它没有)。

但是,不知何故,calc()函数中的这个语句弄乱了JIT:

        c1 ^= z + c2;

不知何故,这为JIT增加了太多的复杂性,以决定最终所有这些代码都不会改变任何东西,并且可以优化原始循环。

如果您将该特定陈述更改为更无意义的内容,例如:

        c1 = z + c2;

然后JIT选择并优化你的循环。试试看。 : - )

我尝试使用更小的数据集进行本地测试,并且“^ =”版本计算需要大约1.6秒,而使用“=”版本则需要0.007秒(换句话说,它优化了循环)

正如我所说,并非真正的回应,但我认为这可能很有趣。

答案 3 :(得分:0)

您是否尝试“内联”parse()和calc(),即将所有代码放在main()中?

答案 4 :(得分:0)

如果在列表迭代中移动calc函数的几行,得分是多少? 我知道它不是很干净,但你会获得调用堆栈。

[...]
    for (int k = 0; k < list.length; k++) {
        int a1 = list[k++];
        int a2 = list[k++];
        int b1 = list[k++];
        int b2 = list[k];

        int c1 = (a1 + a2) ^ a2;
        int c2 = (b1 - b2) << 4;

        for (int z = 0; z < 100; z++) {
            c1 ^= z + c2;
        }

        rs = c1;
    }

答案 5 :(得分:0)

MappedByteBuffer只占I / O性能的20%左右,这是一个巨大的内存成本 - 如果它导致交换,治愈效果会比疾病更糟。

我会在FileReader周围使用一个BufferedReader,也可能在它周围使用一个Scanner来获取整数,或者至少是Integer.parseInt(),这比HotSpot更可能比你自己的基数转换更热代码。

答案 6 :(得分:0)

  

我正在尝试测试Java执行简单任务的速度:将大文件读入内存,然后对数据执行一些无意义的计算。

如果任务是进行无意义的计算,那么最佳优化是不进行计算

如果你真正想要做的就是弄清楚是否有一种通用的技术可以使计算更快,那么我认为你正在咆哮错误的树。没有这样的技术。您在优化无意义计算方面所学到的知识不太可能适用于其他(希望有意义的)计算。

如果计算 毫无意义,目的是让整个程序更快,你可能已经达到了优化浪费时间的程度

  • 当前(Java) - 26.50s + 11.27s = ~38秒
  • 目标(C ++) - ~26.5s + 4.50 = ~31秒
  • 加速百分比 - 低于20%。

对于~40秒的计算,加速比小于20%可能不值得。让用户在额外的7秒钟内转动拇指更便宜。


这也告诉你一些有趣的事情。在这种情况下,无论您使用的是C ++还是Java,相对而言,它都不会产生很大的差异。该程序的整体性能主要取决于C ++和Java具有可比性的阶段。