【问题标题】:Is multiplication faster than array access?乘法比数组访问快吗?
【发布时间】:2014-02-04 09:06:52
【问题描述】:

令我惊讶的是,与原来的 8 毫秒相比,通过在数组中预生成结果来“优化”乘法时,我得到了更长的时间(10 毫秒)。这只是 Java 的怪癖还是 PC 架构的一般性?我有一个带有 Java 7、Windows 8 64 位的 Core i5 760。

public class Test {
    public static void main(String[] args)  {
        long start = System.currentTimeMillis();
        long sum=0;
        int[] sqr = new int[1000];
        for(int a=1;a<1000;a++) {sqr[a]=a*a;}

        for(int b=1;b<1000;b++)
//          for(int a=1;a<1000;a++) {sum+=a*a+b*b;}
            for(int a=1;a<1000;a++) {sum+=sqr[a]+sqr[b];}
        System.out.println(System.currentTimeMillis()-start+"ms");
        System.out.println(sum);
    }
}

【问题讨论】:

  • 那个测量值太短了,几乎不可能准确。这些数字本质上是纯粹随机的。不要把库存放在里面。而且 Java JIT 优化器可能甚至不会启动 1000 次迭代,所以即使时钟非常准确,测量在实际程序的上下文中也毫无意义。
  • 但是:请记住,数组访问通常涉及乘法(或至少移位)、加法和提取。完全不清楚的是,在简单的整数情况下,这将比简单的乘法执行得更好。
  • 在我的例子中是 4 毫秒(多)与 7 毫秒(添加)。我认为在第一种情况下从堆(java对象内存)访问对象并使用原始类型进行操作的原因是 - 第二种情况。一点处理器花时间不会花时间。
  • 您不想在填充数组后启动计时器吗?否则,这不是一个公平的比较。

标签: java optimization


【解决方案1】:

Konrad Rudolph commented on the issues 进行基准测试。所以我忽略了基准并专注于这个问题:

乘法比数组访问快吗?

是的,很有可能。大约在 20 或 30 年前,情况正好相反。

粗略地说, 你可以在 3 个周期内进行整数乘法(悲观,如果你没有向量指令),如果你直接从L1 缓存,但从那里直下坡。供参考,请参阅


Java 特有的一件事是 pointed out by Ingo 在下面的评论中:您还可以在 Java 中进行边界检查,这使得本已较慢的数组访问变得更慢...

【讨论】:

  • @Ingo 那很好,是的,我完全忘记了:)感谢您指出这一点,并将其添加到答案中。
【解决方案2】:

更合理的基准是:

public abstract class Benchmark {

    final String name;

    public Benchmark(String name) {
        this.name = name;
    }

    abstract int run(int iterations) throws Throwable;

    private BigDecimal time() {
        try {
            int nextI = 1;
            int i;
            long duration;
            do {
                i = nextI;
                long start = System.nanoTime();
                run(i);
                duration = System.nanoTime() - start;
                nextI = (i << 1) | 1;
            } while (duration < 1000000000 && nextI > 0);
            return new BigDecimal((duration) * 1000 / i).movePointLeft(3);
        } catch (Throwable e) {
            throw new RuntimeException(e);
        }
    }

    @Override
    public String toString() {
        return name + "\t" + time() + " ns";
    }

    private static void shuffle(int[] a) {
        Random chaos = new Random();
        for (int i = a.length; i > 0; i--) {
            int r = chaos.nextInt(i);
            int t = a[r];
            a[r] = a[i - 1];
            a[i - 1] = t;
        }
    }


    public static void main(String[] args) throws Exception {
        final int[] table = new int[1000];
        final int[] permutation = new int[1000];

        for (int i = 0; i < table.length; i++) {
            table[i] = i * i;
            permutation[i] = i;
        }
        shuffle(permutation);

        Benchmark[] marks = {
            new Benchmark("sequential multiply") {
                @Override
                int run(int iterations) throws Throwable {
                    int sum = 0;
                    for (int j = 0; j < iterations; j++) {
                        for (int i = 0; i < table.length; i++) {
                            sum += i * i;
                        }
                    }
                    return sum;
                }
            },
            new Benchmark("sequential lookup") {
                @Override
                int run(int iterations) throws Throwable {
                    int sum = 0;
                    for (int j = 0; j < iterations; j++) {
                        for (int i = 0; i < table.length; i++) {
                            sum += table[i];
                        }
                    }
                    return sum;
                }
            },
            new Benchmark("random order multiply") {
                @Override
                int run(int iterations) throws Throwable {
                    int sum = 0;
                    for (int j = 0; j < iterations; j++) {
                        for (int i = 0; i < table.length; i++) {
                            sum += permutation[i] * permutation[i];
                        }
                    }
                    return sum;
                }
            },
            new Benchmark("random order lookup") {
                @Override
                int run(int iterations) throws Throwable {
                    int sum = 0;
                    for (int j = 0; j < iterations; j++) {
                        for (int i = 0; i < table.length; i++) {
                            sum += table[permutation[i]];
                        }
                    }
                    return sum;
                }
            }
        };

        for (Benchmark mark : marks) {
            System.out.println(mark);
        }
    }
}

在我的英特尔核心二重奏上打印(是的,它很旧):

sequential multiply    2218.666 ns
sequential lookup      1081.220 ns
random order multiply  2416.923 ns
random order lookup    2351.293 ns

所以,如果我按顺序访​​问查找数组,这可以最大限度地减少缓存未命中的次数,并允许热点 JVM 优化数组访问的边界检查,那么对于 1000 个元素的数组会有轻微的改进。如果我们对数组进行随机访问,这种优势就会消失。此外,如果表较大,则查找速度会变慢。例如,对于 10000 个元素,我得到:

sequential multiply    23192.236 ns
sequential lookup      12701.695 ns
random order multiply  24459.697 ns
random order lookup    31595.523 ns

因此,数组查找并不比乘法快,除非访问模式(几乎)是顺序的并且查找数组很小。

无论如何,我的测量结果表明,乘法(和加法)只需要 4 个处理器周期(在 2GHz CPU 上每次循环迭代需要 2.3 ns)。你不可能比这更快。此外,除非您每秒执行 50 亿次乘法,否则乘法不会成为您的瓶颈,优化代码的其他部分会更有成效。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2023-03-08
    • 2019-04-07
    • 2013-07-26
    • 1970-01-01
    • 2011-04-30
    • 2013-07-02
    • 2011-04-26
    相关资源
    最近更新 更多