每微秒 1,000,000,000 次计算?
1,000,000,000 calculations per microsecond?
好的,我一直在和一个朋友谈论编译器和程序优化,他建议 n * 0.5
比 n / 2
快。我说过编译器会自动进行这种优化,所以我写了一个小程序来查看 n / 2
和 n * 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;
}
对于这两个版本,我得到了 0.000002s avg。使用 clang main.c -O1
编译时。他说时间测量一定有问题。于是他接着写了一个程序:
#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 seconds\n", 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 seconds\n", 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 seconds\n", 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 seconds\n", m, dT);
return 0;
}
为此他得到了...
1.869570s
1.868254s
25.674016s
3.497555s
...按此顺序。
所以我 运行 我的机器上的程序是用 clang++ main.cpp -O1
编译的,我得到了与以前类似的结果:0.000002 to 0.000011
.
然而,当我在没有优化的情况下编译程序时,我在他第一次测试时得到了与他相似的结果。所以我的问题是,任何数量的优化如何才能使程序 that 更快?
由于代码在循环的每次迭代中没有做任何不同的事情,编译器可以自由地将循环内的代码移到外面(结果将完全相同),并完全删除循环,留下如您所见,您几乎有 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;
}
是一个不引用 i
或 m
并且不包含循环引用的循环,因此编译器可以轻松删除循环语句
这是一个很好的例子,说明对高级语言进行基准测试比对汇编进行基准测试(已经足够困难了)更难。编译器可以而且经常会干扰您的基准测试。
你朋友有一点,除法(实际除法,不只是用C写/
)比乘法慢。对于双精度,延迟比率约为 4,除法不是流水线而乘法是流水线,因此吞吐量比率更差:大约 20。(这些数字是针对 Haswell,但很典型)
整数除法比浮点除法慢,但是对整数使用浮点乘法会导致两次转换。转换还不错,所以总的来说,浮点乘法仍然更快。
但是任何合适的编译器都会将整数除以常量转换为整数乘法和移位,可能还有一些额外的修正内容(取决于除数和类型)。除以二的幂就更简单了。有关详细信息,请参阅 Division by Invariant Integers using Multiplication。例如,考虑
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 个周期。
即使没有任何 "unsafe optimizations",也允许编译器将 f / 2.0
转换为 f * 0.5
,除以 2 的幂 是等价的乘以它的倒数。这不适用于不是 2 的幂的数字。
因此,即使使用至少测量了 某些东西 的基准,它也可能无法测量正确的东西。为了测量延迟浮点除法与乘法,你可以这样写:
.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 得到每个 mulpd
或 divpd
的周期数。时间划分取决于被划分的值,因此它不会给出最一般的结果。
好的,我一直在和一个朋友谈论编译器和程序优化,他建议 n * 0.5
比 n / 2
快。我说过编译器会自动进行这种优化,所以我写了一个小程序来查看 n / 2
和 n * 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;
}
对于这两个版本,我得到了 0.000002s avg。使用 clang main.c -O1
编译时。他说时间测量一定有问题。于是他接着写了一个程序:
#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 seconds\n", 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 seconds\n", 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 seconds\n", 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 seconds\n", m, dT);
return 0;
}
为此他得到了...
1.869570s
1.868254s
25.674016s
3.497555s
...按此顺序。
所以我 运行 我的机器上的程序是用 clang++ main.cpp -O1
编译的,我得到了与以前类似的结果:0.000002 to 0.000011
.
然而,当我在没有优化的情况下编译程序时,我在他第一次测试时得到了与他相似的结果。所以我的问题是,任何数量的优化如何才能使程序 that 更快?
由于代码在循环的每次迭代中没有做任何不同的事情,编译器可以自由地将循环内的代码移到外面(结果将完全相同),并完全删除循环,留下如您所见,您几乎有 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;
}
是一个不引用 i
或 m
并且不包含循环引用的循环,因此编译器可以轻松删除循环语句
这是一个很好的例子,说明对高级语言进行基准测试比对汇编进行基准测试(已经足够困难了)更难。编译器可以而且经常会干扰您的基准测试。
你朋友有一点,除法(实际除法,不只是用C写/
)比乘法慢。对于双精度,延迟比率约为 4,除法不是流水线而乘法是流水线,因此吞吐量比率更差:大约 20。(这些数字是针对 Haswell,但很典型)
整数除法比浮点除法慢,但是对整数使用浮点乘法会导致两次转换。转换还不错,所以总的来说,浮点乘法仍然更快。
但是任何合适的编译器都会将整数除以常量转换为整数乘法和移位,可能还有一些额外的修正内容(取决于除数和类型)。除以二的幂就更简单了。有关详细信息,请参阅 Division by Invariant Integers using Multiplication。例如,考虑
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 个周期。
即使没有任何 "unsafe optimizations",也允许编译器将 f / 2.0
转换为 f * 0.5
,除以 2 的幂 是等价的乘以它的倒数。这不适用于不是 2 的幂的数字。
因此,即使使用至少测量了 某些东西 的基准,它也可能无法测量正确的东西。为了测量延迟浮点除法与乘法,你可以这样写:
.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 得到每个 mulpd
或 divpd
的周期数。时间划分取决于被划分的值,因此它不会给出最一般的结果。