Java不可变类速度慢

11

我需要一些复杂数学库,所以我在使用不可变的复数和可变的复数的库之间犹豫不决。显然,我希望计算速度相对较快(除非它会影响可读性等)。

因此,我创建了一个简单的可变与不可变速度测试:

final class MutableInt {
    private int value;

    public int getValue() {
        return value;
    }

    public void setValue(int value) {
        this.value = value;
    }

    public MutableInt() {
        this(0);
    }

    public MutableInt(int value) {
        this.value = value;
    }   
}

final class ImmutableInt {
    private final int value;

    public ImmutableInt(int value) {
        this.value = value;
    }

    public int getValue() {
        return value;
    }
}

public class TestImmutableSpeed {

    static long testMutable(final int arrLen) {
        MutableInt[] arrMutable = new MutableInt[arrLen];
        for (int i = 0; i < arrMutable.length; ++i) {
            arrMutable[i] = new MutableInt(i);
            for (int j = 0; j < arrMutable.length; ++j) {
                arrMutable[i].setValue(arrMutable[i].getValue() + j);
            }
        }
        long sumMutable = 0;
        for (MutableInt item : arrMutable) {
            sumMutable += item.getValue();
        }
        return sumMutable;
    }

    static long testImmutable(final int arrLen) {
        ImmutableInt[] arrImmutable = new ImmutableInt[arrLen];
        for (int i = 0; i < arrImmutable.length; ++i) {
            arrImmutable[i] = new ImmutableInt(i);
            for (int j = 0; j < arrImmutable.length; ++j) {
                arrImmutable[i] = new ImmutableInt(arrImmutable[i].getValue() + j);
            }
        }
        long sumImmutable = 0;
        for (ImmutableInt item : arrImmutable) {
            sumImmutable += item.getValue();
        }
        return sumImmutable;
    }

    public static void main(String[] args) {
        final int arrLen = 1<<14;

        long tmStart = System.nanoTime();
        System.out.println("sum = " + testMutable(arrLen));
        long tmMid = System.nanoTime();
        System.out.println("sum = " + testImmutable(arrLen));
        long tmEnd = System.nanoTime();

        System.out.println("speed comparison mutable vs immutable:");
        System.out.println("mutable   " + (tmMid - tmStart)/1000000 + " ms");
        System.out.println("immutable " + (tmEnd - tmMid)/1000000 + " ms");
    }
}
您可以调整数组大小,以使测试运行速度过慢/过快。
我使用以下参数运行:-server -Xms256m -XX:+AggressiveOpts 结果如下:
sum = 2199023247360 sum = 2199023247360 可变与不可变的速度比较: 可变 102 毫秒 不可变 1506 毫秒 问题是:我是否缺少某些优化参数,或者不可变版本确实比可变版本慢15倍?
如果是这样,为什么会有人在数学库中编写包含不可变类 Complex 的代码?不可变只是“时髦”但无用吗?
我知道不可变类作为哈希映射键或不能存在竞争条件时更安全,但这些都是可以处理任何地方的特殊情况。

是的,这就是为什么当速度很重要(比如总是)并且我需要拥有对象时,我使用可变包装器。3倍比15倍更可能。 - igr
我之前没有看到还有更多的代码 - 滚动区域内嵌滚动区域。这样更有意义。你不能真的像那样在算法之间来回切换进行微基准测试。即使你在一个大循环中多次执行也不行。 - Tom Hawtin - tackline
@ɹoƃı 比率是 (分配成本) / (每个分配的计算速度),因此它随着算法和类别的不同而变化。对于这种特定情况,"int" 的速度非常快,比率更高。 - peenut
3个回答

6
这很有趣。首先,那不是一个公平的测试;你没有在这个过程中热身JVM。进行基准测试通常非常困难。我重构了你的代码,使用Google Caliper,得到了类似但不同的结果;不可变类只慢了3倍。还不确定原因。无论如何,以下是目前的工作进展:
import com.google.caliper.Runner;
import com.google.caliper.SimpleBenchmark;

public class TestImmutableSpeed {
    static final class MutableInt {
        private int value;

        public int getValue() {
            return value;
        }

        public void setValue(int value) {
            this.value = value;
        }

        public MutableInt() {
            this(0);
        }

        public MutableInt(int value) {
            this.value = value;
        }   
    }

    static final class ImmutableInt {
        private final int value;

        public ImmutableInt(int value) {
            this.value = value;
        }

        public int getValue() {
            return value;
        }
    }

    public static class TestBenchmark extends SimpleBenchmark {
        public void timeMutable(final int arrLen) {
            MutableInt[] arrMutable = new MutableInt[arrLen];
            for (int i = 0; i < arrMutable.length; ++i) {
                arrMutable[i] = new MutableInt(i);
                for (int j = 0; j < arrMutable.length; ++j) {
                    arrMutable[i].setValue(arrMutable[i].getValue() + j);
                }
            }
            long sumMutable = 0;
            for (MutableInt item : arrMutable) {
                sumMutable += item.getValue();
            }
            System.out.println(sumMutable);
        }

        public void timeImmutable(final int arrLen) {
            ImmutableInt[] arrImmutable = new ImmutableInt[arrLen];
            for (int i = 0; i < arrImmutable.length; ++i) {
                arrImmutable[i] = new ImmutableInt(i);
                for (int j = 0; j < arrImmutable.length; ++j) {
                    arrImmutable[i] = new ImmutableInt(arrImmutable[i].getValue() + j);
                }
            }
            long sumImmutable = 0;
            for (ImmutableInt item : arrImmutable) {
                sumImmutable += item.getValue();
            }
            System.out.println(sumImmutable);
        }
    }

    public static void main(String[] args) {
        Runner.main(TestBenchmark.class, new String[0]);
    }
}

卡尺输出

 0% Scenario{vm=java, trial=0, benchmark=Immutable} 78574.05 ns; σ=21336.61 ns @ 10 trials
 50% Scenario{vm=java, trial=0, benchmark=Mutable} 24956.94 ns; σ=7267.78 ns @ 10 trials

 benchmark   us linear runtime
 Immutable 78.6 ==============================
   Mutable 25.0 =========

 vm: java
 trial: 0

字符串更新

我认为这个问题需要更深入的思考,所以我决定尝试将被包装的类从int改为一个对象,也就是String。将静态类改为String,并使用Integer.valueOf(i).toString()加载字符串,在StringBuilder中不再添加,而是附加它们,我得到了以下结果:

 0% Scenario{vm=java, trial=0, benchmark=Immutable} 11034616.91 ns; σ=7006742.43 ns @ 10 trials
50% Scenario{vm=java, trial=0, benchmark=Mutable} 9494963.68 ns; σ=6201410.87 ns @ 10 trials

benchmark    ms linear runtime
Immutable 11.03 ==============================
  Mutable  9.49 =========================

vm: java
trial: 0

然而,在这种情况下,我认为差异主要由所有必须进行的数组复制所占主导地位,而不是使用 String 的事实。

你能否发布一个不使用Caliper的相同长度数组的结果,以查看它是否会使可变版本运行更慢(总毫秒数)?我尝试了一些优化参数,其中一些使得JVM对于不可变和可变版本都运行得更慢(但在这种情况下,可变版本并没有快多少)。 - peenut
@peenut,我不理解你的请求,因为你没有使用Caliper。Caliper会自动运行测试,使用相同的数组长度;实际上,在预热JVM后,它会在不同的长度上运行10次。 - durron597
@peenut 很抱歉,我必须回去工作了。它可以在Maven上获得,你可以在这里手动下载:http://search.maven.org/#artifactdetails|com.google.caliper|caliper|0.5-rc1|jar - durron597
1
谢谢,我测试了一下,确实比这个基准慢了3倍,而不是15倍。 - peenut
我去掉了+1,"字符串更新"听起来像个糟糕的想法(使用String来连接字符串而不是StringBuilder,并测量速度?!)。我建议您在"字符串更新"部分发布一些代码或将其删除。 - peenut
显示剩余4条评论

4
不可变值使Java中的干净编程更加简单。您不必在各处复制以避免出现不良影响(我指一处更改值无意中更改了另一处值)。删除副本可以加快速度,但在其他领域创建新实例会降低速度。
(C ++很有趣,因为它采用相反的方法。您可以在没有编写任何代码的情况下在定义良好的点上获得副本。事实上,您必须编写代码才能删除复制。)
如果您关心性能,则可变复杂结构也不好。最好使用一个复杂数组类,它使用单个双精度数组隐藏在实现中,或者只使用双精度数组原始数据。
早在90年代,Guy Steele提到了将值类型添加到Java中作为使语言本身完整的一部分的想法。尽管那是一个非常有限的建议,但类似于后来引入的C#结构体,但两者都无法处理Java中可能最明显的值类,即字符串。

1
所以,基本上,在Java中可变的是慢的,但不可变的则非常慢? - peenut
对于数值计算,Java 中小对象通常很慢。即使在 C 或 C++ 中,你也通常会分配大量小值的数组,而不是单独的堆分配,尽管对于小对象,放置在堆栈上效果很好。 - Tom Hawtin - tackline
Hawtin:C/C++ 版本更快,因为你可以创建值数组,而不是值指针数组——引用局部性更好。我无法想象为什么有人会在 C++ 中为了问题中提供的示例而在堆上分配空间。 - peenut
哦,我应该注意到我加了+1,因为这是一个好观点,我同意。 - peenut
我测试了int[]和MutableInt[]的速度,它们的速度几乎相同。MutableInt的速度足够快(只是需要更多的内存)。 - peenut

1

不可变性有时会带来速度惩罚。如果速度很重要,请使用具有可变复数的数学库。


1
我再次回顾了这个问题,发现你的“Immutable”情况在内部循环的每次迭代中都会构造一个新对象,而“Mutable”则不会。因此,这更多地涉及对象的构造/销毁,特别是在非常严格的内存设置下(因为垃圾收集器将更频繁地运行)。我进行了一个测试,两个版本使用相同的行为,结果“Immutable”版本实际上更快。 - durron597
当然,对于不可变类来说,新对象的构造是瓶颈,这就是问题的关键。如果存在“唯一类型”或者使用局部可见变量(以及可以为其进行优化的jvm,这在官方vm中并非如此),则可以避免构造/销毁。 - peenut

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