我用c++ (Visual Studio 2019 in Release mode)写了这个简单的循环,它遍历一百万个32位整数,并将每个整数分配给前一个:
for ( int i = 1; i < Iters; i++ )
ints32[i - 1] = ints32[i];
这里有一个相同的循环除了它使用了+=
:
for ( int i = 1; i < Iters; i++ )
ints32[i - 1] += ints32[i];
我通过运行它们20次来测量这两个函数,丢弃5个最慢和5个最快的运行,并取其余的平均值。(在每次测量之前,我用随机整数填充数组。)我发现只执行赋值的循环大约需要460微秒,但是执行加法的循环需要320微秒。这些测量值在多次运行中是一致的(它们有一点不同,但加法总是更快),即使我改变顺序其中我测量了两个函数
为什么循环与增加的速度比没有添加循环?如果有什么不同的话,我认为这个添加会让它花更长的时间。
这是反汇编,你可以看到函数是等效的,除了加法循环做了更多的工作(并将eax
设置为ints[i - 1]
而不是ints[i]
)。
for ( int i = 1; i < Iters; i++ )
00541670 B8 1C 87 54 00 mov eax,54871Ch
ints32[i - 1] = ints32[i];
00541675 8B 08 mov ecx,dword ptr [eax]
00541677 89 48 FC mov dword ptr [eax-4],ecx
0054167A 83 C0 04 add eax,4
0054167D 3D 18 90 91 00 cmp eax,offset floats32 (0919018h)
00541682 7C F1 jl IntAssignment32+5h (0541675h)
}
00541684 C3 ret
for ( int i = 1; i < Iters; i++ )
00541700 B8 18 87 54 00 mov eax,offset ints32 (0548718h)
ints32[i - 1] += ints32[i];
00541705 8B 08 mov ecx,dword ptr [eax]
00541707 03 48 04 add ecx,dword ptr [eax+4]
0054170A 89 08 mov dword ptr [eax],ecx
0054170C 83 C0 04 add eax,4
0054170F 3D 14 90 91 00 cmp eax,919014h
00541714 7C EF jl IntAddition32+5h (0541705h)
}
00541716 C3 ret
(int数组是volatile
,因为我不想让编译器将其优化或向量化,实际上它产生的反汇编是我想要测量的。)
编辑:我注意到,当我改变有关程序的看似无关的事情时,赋值版本变得更快,然后又变慢。我怀疑这可能与函数代码的对齐有关,也许?
我使用默认的Visual Studio 2019 Win32版本编译器选项(从项目属性中复制):
/permissive- /ifcOutput "Release" /GS /GL /analyze- /W3 /Gy /Zc:wchar_t /Zi /Gm- /O2 /sdl /Fd"Releasevc142.pdb" /Zc:inline /fp:precise /D "WIN32" /D "NDEBUG" /D "_CONSOLE" /D "_UNICODE" /D "UNICODE" /errorReport:prompt /WX- /Zc:forScope /Gd /Oy- /Oi /MD /FC /Fa"Release" /EHsc /nologo /Fo"Release" /Fp"Releaseprofile.pch" /diagnostics:column
编译器版本:Microsoft Visual Studio Community 2019 version 16.11.15
完整的代码如下:
#include <iostream>
#include <chrono>
#include <random>
#include <algorithm>
const int ValueRange = 100000000;
std::default_random_engine generator;
std::uniform_int_distribution< int > distribution( 1, ValueRange - 1 );
const int Iters = 1000000; // nanoseconds -> milliseconds
volatile int ints32[Iters];
void InitArrayInt32()
{
for ( int i = 0; i < Iters; i++ )
ints32[i] = distribution( generator );
}
const int SampleCount = 20;
const int KeepSampleCount = SampleCount - 2 * (SampleCount / 4);
float ProfileFunction( void(*setup)(), void(*func)() )
{
uint64_t times[SampleCount];
for ( int i = 0; i < SampleCount; i++ )
{
setup();
auto startTime = std::chrono::steady_clock::now();
func();
auto endTime = std::chrono::steady_clock::now();
times[i] = std::chrono::duration_cast<std::chrono::microseconds>( endTime - startTime ).count();
}
std::sort( times, times + SampleCount );
uint64_t total = 0;
for ( int i = SampleCount / 4; i < SampleCount - SampleCount / 4; i++ )
total += times[i];
return total * (1.0f / KeepSampleCount);
}
void IntAssignment32()
{
for ( int i = 1; i < Iters; i++ )
ints32[i - 1] = ints32[i];
}
void IntAddition32()
{
for ( int i = 1; i < Iters; i++ )
ints32[i - 1] += ints32[i];
}
int main()
{
float assignment = ProfileFunction( InitArrayInt32, IntAssignment32 );
float addition = ProfileFunction( InitArrayInt32, IntAddition32 );
printf( "assignment: %gn", assignment );
printf( "addition: %gn", addition );
return 0;
}
一个人比另一个人快的原因是运气。由代码和数据对齐引起的速度差异比生成的代码中的微小差异影响大得多。编辑:我注意到,当我改变有关程序的看似无关的事情时,赋值版本变得更快,然后又变慢。我怀疑这可能与函数代码的对齐有关,也许?
即使你测量多次,你测量的总是相同的对齐(在某种程度上,如果你有地址空间随机化)。这样可以消除调度器和中断带来的噪声,但不能消除对齐更改带来的噪声。
但是当你改变代码中的某些东西时,你改变了对齐方式,例如将数组移动4个字节。将代码移位1个字节。这对速度的影响比gcc中- 0和-O2的差更大。有很多关于这个效应的论文,甚至更多。