C#基本操作时间如何随数字大小而变化?

问题描述 投票:-1回答:2

这是一个函数,每帧需要运行一次,因此在性能方面非常关键。该函数包含一个循环及其中的操作。

private int MyFunction(int number)
{
    // Code
    for (int i = 0; i <= 10000; i++)
    {
        var value = i * number
        var valuePow2 = value * value;

        // Some code which uses valuePow2 several times
    }
    return 0; // Not actual line
}

现在,由于数学特性,我们知道(a * b)²等于a²*b²

所以,有可能将我的功能变为:

private int MyFunction(int number)
{
    // Code
    var numberPow2 = number * number;
    for (int i = 0; i <= 10000; i++)
    {
        var iPow2 = i * i
        var valuePow2 = numberPow2 * iPow2;

        // Some code which uses valuePow2 several times
    }
    return 0; // Not actual line
}

直观地说,这似乎应该更快,因为数字2不变,现在只在循环外计算一次。至少,这对人类来说要快得多,因为x²操作在循环期间以更小的数量完成。

我想知道的是,在C#中,当你使用像int这样的类型时,乘法实际上会更快,数字更小吗?

例如,5 * 5的执行速度是否会超过5000 * 5000?

如果是这样,那么第二个版本更好,即使是一小部分,因此。

但是,如果对于给定的数据类型,时间是恒定的,那么函数的第一个版本更好,因为一半的计算将在较小的数字上完成,因为我在循环中执行相同数量的乘法,但是在第二个版本中,我在开始之前做了一次额外的乘法。

我知道,出于所有意图和目的,性能差异可以忽略不计。我在Code Review中建议了第二个版本,因为该函数很关键,我找不到任何文档来支持这两个视图。

c# performance optimization multiplication micro-optimization
2个回答
2
投票

例如,5 * 5的执行速度是否会超过5000 * 5000?

对于编译时常数,5 * x5000 * x便宜,因为前者可以用lea eax, [rdi + rdi*4]完成。

但对于运行时变量,唯一具有数据依赖性能的整数指令是除法。这适用于任何主流CPU:流水线技术非常重要,即使某些情况下可以以较低的延迟运行,它们通常也不会,因为这会使调度更加困难。 (你不能让相同的执行单元在同一个周期内产生2个结果;相反,CPU只是想知道在一个周期内输入输入肯定会导致3个周期后出现的答案。)

(对于FP,同样只有division和sqrt在普通CPU上具有数据相关的性能。)

如果分支以不同的方式使用具有任何数据相关分支的整数或FP的代码可能会慢得多。 (例如,对于二元搜索的一个跳跃序列,“训练”分支预测;使用另一个键进行搜索将会更慢,因为它将至少错误地预测一次。)

并且为了记录,使用Math.Pow而不是整数*的建议是疯狂的。简单地将整数转换为double并返回比使用整数乘法本身更慢。


亚当的答案链接了一个基于大数组的基准,可以实现自动矢量化。 SSE / AVX2只有32位整数乘法。而64位需要更多的内存带宽。这也是为什么它显示16位和8位整数的加速比。所以它发现c=a*b在Haswell CPU上以半速运行,但这不适用于你的循环情况。

在标量代码中,imul r64, r64在英特尔主流CPU(至少是Nehalem)和Ryzen(imul r32, r32)上与https://agner.org/optimize/具有相同的性能。 1个uop,3个周期延迟,1个/时钟吞吐量。

它只是AMD Bulldozer系列,AMD Atom和Silvermont,64位标量乘法更慢。 (当然假设是64位模式!在32位模式下,使用64位整数的速度较慢。)


优化你的循环

对于number的固定值,编译器可以并且将优化它到i*number,而不是重新计算inum += number。这被称为strength-reduction optimization,因为加法比乘法更“弱”(稍微便宜)。

for(...) {
    var value = i * number
    var valuePow2 = value * value;
}

可以编译成asm做类似的东西

var value = 0;
for(...) {
    var valuePow2 = value * value;

    ...

    value += number;
}

如果编译器没有为您执行此操作,您可以尝试以这种方式手动编写它。

但整数乘法非常便宜,并且在现代CPU上完全流水线化,尤其如此。它的延迟略高于添加,并且可以在更少的端口上运行(通常每个时钟吞吐量只有1个而不是4个用于添加),但是你说你在使用valuePow2做了大量工作。这应该让无序执行隐藏延迟。


如果检查asm并且编译器正在使用递增1的单独循环计数器,您还可以尝试手动保持编译器优化循环以使用value作为循环计数器。


var maxval = number * 10000;
for (var value = 0; i <= maxval; value += number) {
    var valuePow2 = value * value;

    ...
}

如果number*10000可以溢出,请小心,如果你需要它正确包装。在这种情况下,此循环将运行更少的迭代。 (除非number是如此之大,以至于value += number也包裹......)


1
投票

对于典型的处理器,无论这些整数中的数据如何,乘以两个32位整数将占用相同的周期数。 Most current processors will take nearly twice the time to multiply 64-bit integers as it takes to multiply 32-bit integers.

我确实注意到你的两个代码都有问题。当你乘以两个整数时,它返回一个int类型。 var类型将类型设置为返回值。这意味着,valuePow2将是一个int。由于你的循环达到10000,如果数字是5或更大,那么你将溢出valuePow2。

如果您不想溢出int,可以将代码更改为

private int MyFunction(int number)
{
    // Code
    for (int i = 0; i <= 10000; i++)
    {
        long value = i * number;        //64bit multiplication          
        long valuePow2 = value * value; //64bit multiplication

        // Some code which uses valuePow2 several times
    }
    return 0; // Not actual line
}

修改后的代码应该更快,因为您可以将64位乘法更改为32位乘法

private int MyFunction(int number)
{
    // Code
    long numberPow2 = number * number; //64bit multiplication
    for (int i = 0; i <= 10000; i++)
    {
        int iPow2 = i * i;                      //32bit multiplication
        long valuePow2 = numberPow2 * iPow2;    //64bit multiplication

        // Some code which uses valuePow2 several times
    }
    return 0; // Not actual line
}

但是CPU中的电路和编译器的优化可以改变最终运行的循环次数。在一天结束时,你说得最好:

我知道,出于所有意图和目的,性能差异可以忽略不计。

© www.soinside.com 2019 - 2024. All rights reserved.