我有一段代码在windows上比在linux上运行快2倍,下面是我测量的时间:
g++ -Ofast -march=native -m64
29.1123
g++ -Ofast -march=native
29.0497
clang++ -Ofast -march=native
28.9192
visual studio 2013 Debug 32b
13.8802
visual studio 2013 Release 32b
12.5569
似乎真的差别太大了。
下面是代码:
#include <iostream>
#include <map>
#include <chrono>
static std::size_t Count = 1000;
static std::size_t MaxNum = 50000000;
bool IsPrime(std::size_t num)
{
for (std::size_t i = 2; i < num; i++)
{
if (num % i == 0)
return false;
}
return true;
}
int main()
{
auto start = std::chrono::steady_clock::now();
std::map<std::size_t, bool> value;
for (std::size_t i = 0; i < Count; i++)
{
value[i] = IsPrime(i);
value[MaxNum - i] = IsPrime(MaxNum - i);
}
std::chrono::duration<double> serialTime = std::chrono::steady_clock::now() - start;
std::cout << "Serial time = " << serialTime.count() << std::endl;
system("pause");
return 0;
}
所有这些测试都是在同一台机器上进行的,使用的是windows8和linux3.19.5(gcc4.9.2,clang3.5.0),linux和windows都是64位的。
这可能是什么原因?一些调度问题?
3条答案
按热度按时间u5rb5r591#
你不会说windows/linux操作系统是32位还是64位。
在一台64位linux机器上,如果你把size_t改为int,你会发现linux上的执行时间下降到与windows相似的值。
size_t在win32上是一个int 32,在win 64上是一个int 64。
编辑:刚刚看到你的Windows拆卸。
您的Windows操作系统是32位的(或者至少您已经为32位编译过)。
r1wp621o2#
size_t
是Linux上x86-64 System V ABI中的一个64位无符号类型,您在其中编译64位二进制文件。(就像你在Windows上做的一样),它只有32位,因此试除循环只做32位除法。(size_t
是针对C++对象的大小,而不是文件的大小,因此它只需要是指针宽度。)在x86-64 Linux上,
-m64
是默认值,因为32位基本上被认为是过时的。要创建32位可执行文件,请使用g++ -m32
。与大多数整数运算不同,Ice Lake之前英特尔CPU上的除法吞吐量(和延迟)取决于操作数大小:64位除法比32位除法慢。(https://agner.org/optimize/和https://uops.info/用于查看哪些端口的指令吞吐量/延迟/微指令表)。相关:代码评审问答:AMD没有这个问题(或者64位div/idiv不合理地慢),Intel Ice Lake使64位div/idiv的微指令数与32位相似。
与其他运算(如乘法,尤其是加法)相比,它的速度非常慢:你的程序完全在整数除法吞吐量上瓶颈,而不是在
map
操作上。(使用Skylake上32位二进制的性能计数器,arith.divider_active
可统计24.03
十亿个周期,其中除法执行单元处于活动状态,而总内核时钟周期数为24.84
十亿。是的,没错,除法运算非常慢,所以只有一个性能计数器专门针对那个执行单元。它也是一个特殊情况,因为它不是完全流水线的,所以即使在这样的情况下,你有独立的除法运算,它不能像其它多周期运算(如FP或整数乘法)那样,在每个时钟周期启动一个新运算。)不幸的是,g++无法基于数字是编译时常量的事实进行优化,因此范围有限法律的
g++ -m64
优化到div ecx
而不是div rcx
(并且有巨大的加速),这一改变使得64位二进制文件的运行速度与32位二进制文件一样快。(它的计算过程完全相同,只是没有那么多高0位,结果被隐式地零扩展以填充64位寄存器,而不是被除法器显式地计算为零,在这种情况下,这样做要快得多。)我在Skylake上验证了这一点,方法是编辑二进制文件,将
0x48
雷克斯.W前缀替换为0x40
,将div rcx
更改为div ecx
,并使用不执行任何操作的REX前缀。所用的总周期在g++ -O3 -m32 -march=native
的32位二进制文件的1%以内。(还有时间,因为CPU在两次运行中碰巧以相同的时钟速度运行)(Godbolt编译器资源管理器上的g++7.3 asm输出)。运行Linux的3.9GHz Skylake i7- 6700 k上的32位代码gcc7.3 -O3
64位,雷克斯.W=0(手动编辑的二进制)
与原始64位二进制相比:
IDK为什么
arith.divider_active
的性能计数器没有上升更多。div 64
比div r32
有更多的微指令,所以 * 可能 * 它损害了无序执行并减少了周围代码的重叠。但我们知道,没有其他指令的连续div
具有类似的性能差异。无论如何,这段代码大部分时间都花在了可怕的试除循环上(它检查每个奇数和偶数除数,即使我们在检查低位后已经排除了所有偶数除数......而且它一直检查到
num
而不是sqrt(num)
,所以对于非常大的素数来说,它的速度非常慢)。根据
perf record
,99.98%的cpu周期事件是在 * 第二个 * 试除循环中触发的,也就是MaxNum - i
,所以div
仍然是整个瓶颈,这只是性能计数器的一个怪癖,并不是所有时间都记录为arith.divider_active
从Agner Fog的Skylake指令表中可以看出:
(
div r64
本身实际上依赖于其输入的实际大小,输入越小越快。* 真正 * 慢的情况是具有非常大的商IIRC。当RDX:RAX中128位被除数的上半部分为非零时,可能也会更慢。C编译器通常只将div
与rdx=0
一起使用。)周期计数的比率(
78733701858 / 24938804081 = ~3.15
)实际上小于最佳情况吞吐量的比率(21/6 = 3.5
)。这应该是一个纯粹的吞吐量瓶颈,而不是延迟,因为下一个循环迭代可以在不等待最后一个除法结果的情况下开始。(多亏了分支预测+推测执行。)该除法循环中可能有一些分支未命中。如果你只找到了2倍的性能比,那么你有一个不同的CPU。可能是Haswell,其中32位
div
吞吐量是9-11个周期,64位div
吞吐量是21-74。可能不是AMD:即使是
div r64
,最佳情况下的吞吐量仍然很小。例如,Steamroller的吞吐量为div r32
= 1/13-39周期,div r64
= 13-70。我猜想,对于相同的实际数字,即使将它们分配给更宽寄存器中的除法器,也可能获得相同的性能。不像英特尔。(最坏情况上升是因为输入和结果的可能大小更大。)AMD整数除法仅为2uop,不像英特尔的在Skylake上被微编码为10或36 uops。(对于57 uops的带符号idiv r64
甚至更高。)这可能与AMD在宽寄存器中对小数目的高效性有关。顺便说一句,FP除法始终是单微指令,因为它在普通代码中对性能要求更高。(提示:在真实的生活中,如果他们关心性能的话,没有人会使用完全幼稚的试除来检查多个素数。
有序
map
的键是size_t
,64位代码中的指针更大,使得每个红黑树节点明显更大,但这不是瓶颈。顺便说一句,与两个
bool prime_low[Count], prime_high[Count]
阵列相比,map<>
是一个 * 糟糕 * 的选择:一个用于低Count
元素,一个用于高Count
元素。您有2个连续的范围,键可以通过位置隐式。或者至少使用std::unordered_map
哈希表。我觉得有序版本应该称为ordered_map
,和map = unordered_map
,因为您经常看到使用map
的代码没有利用排序。您甚至可以使用
std::vector<bool>
来获取位图,使用1/8该高速缓存空间。有一个“x32”ABI(长模式下的32位指针),它具有两个方面的优点,适用于不需要超过4G虚拟地址空间的进程:小指针可以提高数据密度/减少指针密集型数据结构中的缓存占用空间,但它具有现代调用约定、更多寄存器、基线SSE 2和64位整数寄存器(用于需要64位数学运算时)的优势。但遗憾的是,它并不是很受欢迎。它只是稍微快一点,所以大多数人不希望每个库都有第三个版本。
在这种情况下,您可以将源代码修复为使用
unsigned int
(或者uint32_t
,如果你想移植到int
只有16位的系统上)。或者uint_least32_t
,以避免需要固定宽度的类型。你可以只对IsPrime
的arg这样做,或者也可以对数据结构这样做。(但如果您正在优化,则键是根据数组中的位置隐式设置的,而不是显式设置的。)您甚至可以创建一个具有64位循环和32位循环的
IsPrime
版本,后者根据输入的大小进行选择。omjgkv6w3#
从编辑的问题中提取答案:
这是由于在Windows上构建32b二进制文件而不是在Linux上构建64b二进制文件造成的,以下是Windows的64b数字: