每微秒1000000000次计算

1,000,000,000 calculations per microsecond?

本文关键字:计算 1000000000次      更新时间:2023-10-16

好吧,我一直在和一个朋友谈论编译器和程序优化,他建议n * 0.5n / 2快。我说过编译器会自动进行这种优化,所以我写了一个小程序来看看n / 2n * 0.5之间是否有区别:

部门:

#include <stdio.h>
#include <time.h>
int main(int argc, const char * argv[]) {
    int i, m;
    float n, s;
    clock_t t;
    m = 1000000000;
    t = clock();
    for(i = 0; i < m; i++) {
        n = i / 2;
    }
    s = (float)(clock() - t) / CLOCKS_PER_SEC;
    printf("n = i / 2: %d calculations took %f seconds (last calculation = %f)n", m, s, n);
    return 0;
}

乘法:

#include <stdio.h>
#include <time.h>
int main(int argc, const char * argv[]) {
    int i, m;
    float n, s;
    clock_t t;
    m = 1000000000;
    t = clock();
    for(i = 0; i < m; i++) {
        n = i * 0.5;
    }
    s = (float)(clock() - t) / CLOCKS_PER_SEC;
    printf("n = i * 0.5: %d calculations took %f seconds (last calculation = %f)n", m, s, n);
    return 0;
}

对于这两个版本,当使用clang main.c -O1编译时,我得到了0.000002s的平均值。他说时间测量肯定有问题。于是他写了一个程序:

#include <cstdio>
#include <iostream>
#include <ctime>
using namespace std;
int main()
{
    clock_t ts, te;
    double  dT;
    int i, m;
    double n, o, p, q, r, s;
    m = 1000000000;
    cout << "Independent calculations:n";
    ts = clock();
    for (i = 0; i < m; i++)
    {
        //  make it a trivial pure float calculation with no int casting to float
        n = 11.1 / 2.3;
        o = 22.2 / 2.3;
        p = 33.3 / 2.3;
        q = 44.4 / 2.3;
        r = 55.5 / 2.3;
        s = 66.6 / 2.3;
    }
    te = clock();
    dT = ((float)(te - ts)) / CLOCKS_PER_SEC;   //  make initial call to get the elapsed time to run the loop
    ts = clock();
    printf("Division: %d calculations took %f secondsn", m, dT);
    for (i = 0; i < m; i++)
    {
        //  make it a trivial pure float calculation with no int casting to float
        n = 11.1 * 0.53;
        o = 22.2 * 0.53;
        p = 33.3 * 0.53;
        q = 44.4 * 0.53;
        r = 55.5 * 0.53;
        s = 66.6 * 0.53;
    }
    te = clock();
    dT = ((float)(te - ts)) / CLOCKS_PER_SEC;   //  make initial call to get the elapsed time to run the loop
    ts = clock();
    printf("Multiplication: %d calculations took %f secondsn", m, dT);
    cout << "nDependent calculations:n";
    for (i = 0; i < m; i++)
    {
        //  make it a trivial pure float calculation with no int casting to float
        n = 11.1 / 2.3;
        o = n / 2.3;
        p = o / 2.3;
        q = p / 2.3;
        r = q / 2.3;
        s = r / 2.3;
    }

    te = clock();
    dT = ((float)(te - ts)) / CLOCKS_PER_SEC;   //  make initial call to get the elapsed time to run the loop
    ts = clock();
    printf("Division: %d calculations took %f secondsn", m, dT);
    for (i = 0; i < m; i++)
    {
        //  make it a trivial pure float calculation with no int casting to float
        n = 11.1 * 0.53;
        o = n * 0.53;
        p = o * 0.53;
        q = p * 0.53;
        r = q * 0.53;
        s = r * 0.53;
    }

    te = clock();
    dT = ((float)(te - ts)) / CLOCKS_PER_SEC;   //  make initial call to get the elapsed time to run the loop
    ts = clock();
    printf("Multiplication: %d calculations took %f secondsn", m, dT);
    return 0;
}

为此,他得到了。。。

1.869570s
1.868254s
25.674016s
3.497555s

按照这个顺序。

所以我在用clang++ main.cpp -O1编译的机器上运行了这个程序,得到了和以前类似的结果:0.000002 to 0.000011

然而,当我在没有优化的情况下编译程序时,我在他的第一次测试中得到了与他相似的结果。所以我的问题是,任何数量的优化如何使的程序更快?

由于代码在循环的每次迭代中都没有做任何不同的事情,编译器可以自由地将循环中的代码移到外部(结果将完全相同),并完全删除循环,留下几乎为0的运行时,如您所见。

for (i = 0; i < m; i++)
{
    //  make it a trivial pure float calculation with no int casting to float
    n = 11.1 * 0.53;
    o = n * 0.53;
    p = o * 0.53;
    q = p * 0.53;
    r = q * 0.53;
    s = r * 0.53;
}

是一个不引用im的循环,并且不包含循环引用,因此编译器删除循环语句

是很简单的

这是一个很好的例子,说明了对高级语言进行基准测试比对程序集进行基准测试更难(这已经足够难了)。编译器可以而且经常会干扰您的基准测试。

你的朋友有一点,除法(实际的除法,而不仅仅是用C写/)比乘法慢。对于doubles,延迟的比率约为4,除法不是流水线运算,而乘法是流水线运算,因此吞吐量比率要差得多:约为20。(这些数字适用于Haswell,但很典型)

整数除法比浮点除法慢,但对整数使用浮点乘法会导致两次转换。转换还不错,所以总的来说,浮点乘法仍然更快。

但是,任何合适的编译器都会将常数的整数除法转换为整数乘法和移位,也许还会添加一些额外的修复内容(取决于除数和类型)。二次幂除法更简单。有关详细信息,请参见使用乘法的不变整数除法。例如,考虑

int div2(int i)
{
    return i / 2;
}

GCC将其转化为

mov eax, edi
shr eax, 31
add eax, edi
sar eax
ret

这取决于µarch,需要3或4个循环(不包括控制流)。

另一方面,

int div2(int i)
{
    return i * 0.5;
}

变成

    cvtsi2sd    xmm0, edi
    mulsd   xmm0, QWORD PTR .LC0[rip]
    cvttsd2si   eax, xmm0
    ret
.LC0:
    .long   0
    .long   1071644672

这将需要大约4+5+4=13个循环。

编译器也可以将f / 2.0转换为f * 0.5,即使没有任何"不安全的优化",除以二次方也相当于乘以其逆。这不适用于不是二次幂的数字。

因此,即使使用一个至少测量某些东西的基准,它也可能无法测量正确的东西。为了测量浮点除法与乘法的延迟,你可以写这样的东西:

.section data
align 16
one: dq 1.0, 1.0
.section text
_bench1:
  mov ecx, -10000000
  movapd xmm0, [one]
loopone:
  mulpd xmm0, xmm0
  mulpd xmm0, xmm0
  add ecx, 1
  jnz loopone
  ret
_bench2:
  mov ecx, -10000000
  movapd xmm0, [one]
looptwo:
  divpd xmm0, xmm0
  divpd xmm0, xmm0
  add ecx, 1
  jnz looptwo
  ret

同时致电一千,包裹在rdtsc中获取时间。两者都花最少的时间。将时间乘以基本时钟和涡轮时钟之间的比率。然后,您应该(大约)两个循环所用的循环数,除以20000000,得到每个mulpddivpd的循环数。时间划分取决于被划分的值,因此它不会给出最一般的结果。

您可能还对指令延迟和吞吐量的列表感兴趣。