在英特尔Sandybridge系列CPU中为pipe道优化一个程序

为了完成这个任务,我一直在想我的大脑一个星期,我希望这里有人能带领我走向正确的道路。 让我从讲师的指示开始:

你的任务与我们第一个实验任务相反,那就是优化素数程序。 你在这个任务中的目的是让程序变得悲观,也就是让它运行得更慢。 这两个都是CPU密集型的程序。 他们需要几秒钟在我们的实验室PC上运行。 你不能改变algorithm。

为了使程序最优化,请使用您对Intel i7pipe道运行方式的了解。 想象一下如何重新排列指令path来引入WAR,RAW和其他危险。 想想如何最大限度地减lesscaching的有效性。 是恶魔无能。

这项任务给了Whetstone或Monte-Carlo项目的select。 caching有效性评论大多只适用于Whetstone,但我select了Monte-Carlo模拟程序:

// Un-modified baseline for pessimization, as given in the assignment #include <algorithm> // Needed for the "max" function #include <cmath> #include <iostream> // A simple implementation of the Box-Muller algorithm, used to generate // gaussian random numbers - necessary for the Monte Carlo method below // Note that C++11 actually provides std::normal_distribution<> in // the <random> library, which can be used instead of this function double gaussian_box_muller() { double x = 0.0; double y = 0.0; double euclid_sq = 0.0; // Continue generating two uniform random variables // until the square of their "euclidean distance" // is less than unity do { x = 2.0 * rand() / static_cast<double>(RAND_MAX)-1; y = 2.0 * rand() / static_cast<double>(RAND_MAX)-1; euclid_sq = x*x + y*y; } while (euclid_sq >= 1.0); return x*sqrt(-2*log(euclid_sq)/euclid_sq); } // Pricing a European vanilla call option with a Monte Carlo method double monte_carlo_call_price(const int& num_sims, const double& S, const double& K, const double& r, const double& v, const double& T) { double S_adjust = S * exp(T*(r-0.5*v*v)); double S_cur = 0.0; double payoff_sum = 0.0; for (int i=0; i<num_sims; i++) { double gauss_bm = gaussian_box_muller(); S_cur = S_adjust * exp(sqrt(v*v*T)*gauss_bm); payoff_sum += std::max(S_cur - K, 0.0); } return (payoff_sum / static_cast<double>(num_sims)) * exp(-r*T); } // Pricing a European vanilla put option with a Monte Carlo method double monte_carlo_put_price(const int& num_sims, const double& S, const double& K, const double& r, const double& v, const double& T) { double S_adjust = S * exp(T*(r-0.5*v*v)); double S_cur = 0.0; double payoff_sum = 0.0; for (int i=0; i<num_sims; i++) { double gauss_bm = gaussian_box_muller(); S_cur = S_adjust * exp(sqrt(v*v*T)*gauss_bm); payoff_sum += std::max(K - S_cur, 0.0); } return (payoff_sum / static_cast<double>(num_sims)) * exp(-r*T); } int main(int argc, char **argv) { // First we create the parameter list int num_sims = 10000000; // Number of simulated asset paths double S = 100.0; // Option price double K = 100.0; // Strike price double r = 0.05; // Risk-free rate (5%) double v = 0.2; // Volatility of the underlying (20%) double T = 1.0; // One year until expiry // Then we calculate the call/put values via Monte Carlo double call = monte_carlo_call_price(num_sims, S, K, r, v, T); double put = monte_carlo_put_price(num_sims, S, K, r, v, T); // Finally we output the parameters and prices std::cout << "Number of Paths: " << num_sims << std::endl; std::cout << "Underlying: " << S << std::endl; std::cout << "Strike: " << K << std::endl; std::cout << "Risk-Free Rate: " << r << std::endl; std::cout << "Volatility: " << v << std::endl; std::cout << "Maturity: " << T << std::endl; std::cout << "Call Price: " << call << std::endl; std::cout << "Put Price: " << put << std::endl; return 0; } 

我所做的更改似乎将代码运行时间增加了一秒,但是我不能完全确定我可以更改什么来停止pipe道而不添加代码。 指向正确的方向将是真棒,我感谢任何答复。


更新: 给这个任务的教授贴了一些细节

亮点是:

  • 这是社区学院第二学期的build筑课程(使用轩尼诗和帕特森教科书)。
  • 实验室电脑有Haswell CPU
  • 学生们已经接触到CPUID指令,以及如何确定caching大小,以及内部函数和CLFLUSH指令。
  • 任何编译器选项都是允许的,因此是内联的asm。
  • 编写自己的平方根algorithm被宣布为在苍白之外

Cowmoogun对元线程的评论表明, 并不清楚编译器优化是否可以成为其中的一部分,并假设为-O0 ,运行时间增加17%是合理的。

所以这个任务的目标听起来就是让学生重新安排现有的工作来减less指令级的并行或者类似的事情,但是人们深入研究并学到更多东西并不是什么坏事。


请记住,这是一个计算机体系结构问题,而不是如何使C ++缓慢的问题。

重要的背景阅读: Agner Fog的微型pdf ,也可能是Ulrich Drepper的每个程序员都应该知道的内存 。 另请参阅x86标记wiki中的其他链接,特别是英特尔的优化手册以及David Kanter 对Haswell微体系结构的分析以及图表 。

非常酷的任务; 比我见过的学生要优化一些gcc -O0代码要gcc -O0 ,学习了一堆在真实代码中不重要的技巧。 在这种情况下,您被要求了解CPUpipe道,并使用它来指导您的优化工作,而不仅仅是盲目猜测。 这个最有趣的部分是用“恶魔无能”来certificate每一个悲观,而不是有意的恶意。


与作业的措词和代码有关的问题

这个代码uarch特定的选项是有限的。 它不使用任何数组,大部分成本是调用exp / log库函数。 多less有指令级的并行性,并没有一个明显的方法,循环依赖链很短。

我很想看到一个答案,试图通过重新安排expression式来改变依赖关系,以减less依赖关系(危害)的ILP 。 我没有尝试过。

英特尔Sandybridge系列CPU是具有挑战性的无序devise,花费大量晶体pipe和电源来寻找并行性,避免危害传统RISC有序stream水线的危险(依赖性)。 通常,减慢速度的唯一传统危害是RAW“真实”依赖性,导致吞吐量受延迟的限制。

对寄存器的WAR和WAW危害几乎不是问题,这要归功于寄存器重命名 。 (除了popcnt / lzcnt / tzcnt ,即使它是只写的,也就是说,WAW被作为RAW危险+写入来处理,它们依赖于它们在Intel CPU上的目的地 。 对于内存sorting,现代CPU使用存储队列来延迟提交到caching直到退休,同时避免WAR和WAW危害 。

Nehalem(Core2的后继者)推出了“i7”品牌,英特尔的一些手册甚至在表示Nehalem的时候会说“Core i7”,但是他们保留了Sandybridge的“i7”品牌和后来的微架构。 SnB是当P6家族演化成新的物种,SnB家族 。 在很多方面,Nehalem和Pentium III有许多共同之处,比Sandybridge更普遍(例如,寄存器读取暂停和ROB读取暂停不会发生在SnB上,因为它更改为使用物理寄存器文件,还有一个uopcaching和一个不同的内部uop格式)。 “i7架构”这个术语没有用处 ,因为将SnB族与Nehalem而不是Core2分组是没有意义的。 (Nehalem确实介绍了将多个内核连接在一起的共享包容性三级caching架构,还集成了GPU,因此芯片级别的命名更有意义。


恶魔无能的合理思想总结

即使是恶魔般的无能也不太可能增加明显无用的工作或无限循环,而使C ++ / Boost类变得混乱不在工作范围之内。

  • multithreading与一个共享的 std::atomic<uint64_t>循环计数器,所以正确的总迭代次数发生。 primefacesuint64_t与-m32 -march=i586特别糟糕。 对于奖励点,安排它错位,并跨过一个页面边界不均匀分裂(不是4:4)。
  • 假分享一些其他的非primefacesvariables – >内存顺序错误猜测pipe道清除,以及额外的caching未命中。
  • 不使用FPvariables,而是使用0x80对高字节进行XOR来翻转符号位,导致存储转发暂停
  • 每次迭代的时间都是独立的,比RDTSC还要重。 例如CPUID / RDTSC或进行系统调用的时间函数。 串行化指令本质上是pipe道不友好的。
  • 改变乘常数除以它们的倒数(“为了便于阅读”)。 div是缓慢的,没有完全stream水线。
  • 使用AVX(SIMD)vector化multiply / sqrt,但在调用标量math库exp()log()函数之前,不能使用vzeroupper ,导致AVX < – > SSE转换失速
  • 将RNG输出存储在链接列表中,或存储在不按顺序遍历的数组中。 每次迭代的结果相同,最后加上和。

在这个答案中也包含了这个问题,但是从总结中排除了:在非stream水线CPU上的build议也是一样慢,或者即使在恶意无能的情况下也不合理。 例如许多编译器的想法会产生明显不同的/更糟糕的东西。


multithreading严重

也许使用OpenMP进行multithreading循环的迭代次数很less,比速度增加更多的开销。 但是,您的蒙特卡洛代码具有足够的并行性,实际上可以提高速度。 如果我们成功地使每个迭代变得缓慢。 (每个线程计算一个部分payoff_sum ,在最后添加)。 在这个循环上#omp parallel可能是一个优化,而不是一个悲观。

multithreading但强制两个线程共享相同的循环计数器( atomic增量,所以总的迭代次数是正确的)。 这似乎恶魔般的逻辑。 这意味着使用一个staticvariables作为循环计数器。 这certificate使用循环计数器的atomic ,并创build实际的高速caching行乒乓 (只要线程不运行在同一个超线程的物理核心,可能不会那么慢)。 无论如何,这比lock inc不争的情况慢得多。 并且lock cmpxchg8b以在32位系统上primefaces地增加争用的uint64_t将不得不在循环中重试,而不是硬件仲裁primefacesinc

同时创build错误共享 ,其中多个线程将其私有数据(例如RNG状态)保持在同一caching行的不同字节中。 (英特尔教程,包括性能计数器看看) 。 这里有一个微体系结构特定的方面 :英特尔CPU推测内存错误sorting没有发生,并且有一个内存顺序机器清除perf事件来检测,至less在P4上 。 Haswell的惩罚可能不会那么大。 正如那个链接指出的那样,一个lock指令假定这会发生,避免错误猜测。 正常负载推测其他内核不会在加载执行和按程序顺序退出( 除非使用pause )之间使caching线无效。 真正的共享没有lock指令通常是一个错误。 将一个非primefaces共享循环计数器与primefaces大小写进行比较将会很有趣。 为了保持真正的悲观,保持共享的primefaces循环计数器,并导致其他variables在同一个或不同的caching行虚假共享。


随机uarch特定的想法:

如果你可以引入任何不可预测的分支 ,那将大大地使代码变得悲观。 现代x86 CPU具有相当长的stream水线,因此错误预测花费约15个周期(从uopcaching运行时)。


依赖链:

我认为这是作业的目的之一。

通过select具有一个长依赖链而不是多个短依赖链的操作顺序,击败CPU利用指令级并行的能力。 除非使用-ffast-math ,否则编译器不允许更改FP计算的操作顺序,因为这会改变结果(如下所述)。

要真正做到这一点,增加一个循环运行的依赖链的长度。 尽pipe如此,没有任何东西可以跳出来:所写的循环具有非常短的循环依赖链:只是一个FP添加。 (3个周期)。 多次迭代可以同时进行计算,因为它们可以在上一次迭代结束前的payoff_sum +=之前开始。 ( log()exp需要很多指令,但Haswell的乱序寻找并行性的窗口并不多:ROB大小= 192个融合域uops,调度器大小= 60个未融合域uops 。一旦执行当前迭代的进度足够远以便为来自下一次迭代的指令腾出空间,当旧指令离开执行单元时,其任何部分的input就绪(即,独立/分离的dep链)可以开始执行(例如,因为他们瓶颈在延迟,而不是吞吐量)。

RNG状态几乎可以肯定是一个比addps更长的循环携带依赖链。


使用更慢/更多的FP操作(尤其是更多的分割):

除以2.0而不是乘以0.5,依此类推。 在英特尔的devise中,FP乘法非常stream水,Haswell和后来每0.5c的吞吐量就有一个。 FP divsd / divpd只是部分stream水线 。 (虽然Skylake的每4c吞吐量有一个令人印象深刻的divpd xmm ,有13-14c的延迟,而Nehalem(7-22c)没有stream水线。

do { ...; euclid_sq = x*x + y*y; } while (euclid_sq >= 1.0); do { ...; euclid_sq = x*x + y*y; } while (euclid_sq >= 1.0); 显然是testing一个距离,所以显然这是适当的sqrt()它。 :P( sqrtdiv更慢)。

正如@Paul Clayton所build议的那样,用关联/分配等价物重写expression式可以引入更多的工作(只要不使用-ffast-math来允许编译器重新优化)。 (exp(T*(r-0.5*v*v))可能会变成exp(T*r - T*v*v/2.0)注意:虽然实数的算术是关联的,但是浮点math并不是考虑到溢出/ NaN(这就是为什么-ffast-math在默认情况下不会启用)请参阅Paul对非常多毛的嵌套的pow()build议的评论 。

如果您可以将计算缩小到非常小的数字,那么当两个正常数字的运算产生一个反常规时 ,FPmath运算会花费大约120个额外的周期来捕获微代码 。 请参阅Agner Fog's microarch pdf获取确切的数字和详细信息。 这是不太可能的,因为你有很多乘法,所以比例因子将被平方并且一直下降到0.0。 我看不出有什么办法来certificate无能(甚至是恶魔般)的必要缩放,只有故意的恶意。


如果你可以使用内部函数( <immintrin.h>

使用movnti从caching中movnti数据 。 恶魔:它是新的和弱有序的,所以应该让CPU运行得更快,对吧? 或者看到这个关联的问题,一个人正处于危险的情况下(对于只有一些位置很热的零散写道)。 没有恶意, clflush可能是不可能的。

在FPmath运算之间使用整数混洗来引起旁路延迟。

混合SSE和AVX指令,如果没有正确使用vzeroupper会在Skylake之前造成大的摊位 ( 在Skylake有不同的处罚)。 即使没有这种情况,向量化严重可能会比标量更差(花费更多的周期花费数据进/出向量比一次执行4次蒙特卡洛迭代的add / sub / mul / div / sqrt操作所节省的带有256b向量) 。 add / sub / mul执行单元是完全stream水线和全angular的,但256b向量上的div和sqrt并不像128b向量(或标量)那么快,所以加速对于double来说并不显着。

exp()log()没有硬件支持,所以这部分需要将vector元素提取回标量并分别调用库函数,然后将结果重新组合成一个向量。 通常将libm编译为仅使用SSE2,因此将使用标量math指令的legacy-SSE编码。 如果你的代码使用256b向量,并且先调用exp而不vzeroupper一个vzeroupper ,那么你vzeroupper 。 返回之后,像vmovsd这样的AVX-128指令将下一个向量元素设置为exp的参数也将停止。 然后exp()会在运行SSE指令时再次停顿。 这正是发生在这个问题上 ,导致10倍放缓。 (谢谢@ZBoson)。

另请参阅Nathan Kurz在此代码中使用英特尔的math库与glibc的实验 。 未来的glibc会带有exp()等向量化的实现。


如果以IvB或者esp为目标。 Nehalem试图让gcc使用16位或8位操作导致部分寄存器停顿,接着是32位或64位操作。 在大多数情况下,gcc会在8或16位操作后使用movzx ,但是这里有一个情况,gcc修改ah然后读取ax


With(inline)asm:

使用(内联)asm,可能会破坏uopcaching:不适合3个6uopcaching行的32B代码会强制从uopcaching切换到解码器。 在内部循环内的一个分支目标上使用许多单字节的nop而不是长的nop可能会造成一个不合格的ALIGN 。 或者把alignment填充放在标签之后,而不是之前。 :P这只有当前端是一个瓶颈时才是重要的,如果我们成功地对其余的代码进行了悲观化的话,这将是不成功的。

使用自修改代码来触发pipe道清除(又名机器核)。

LCP从16位指令停止 ,立即数过大,不能适应8位,不太可能有用。 SnB和更高版本上的uopcaching意味着您只需支付一次解码处罚。 在Nehalem(第一个i7)上,它可能适用于不适合28 uop循环缓冲区的循环。 gcc有时会生成这样的指令,即使使用-mtune=intel也可以使用32位指令。


定时的常用方式是CPUID (串行化),然后是RDTSC 。 用CPUID / RDTSC分别进行每次迭代的时间,以确保RDTSC不被先前的指令重新sorting,这会使速度下降很多 。 (在现实生活中,聪明的时间就是把所有的迭代计算在一起,而不是单独计算每个时间并将它们相加)。


导致大量的caching未命中和其他内存减速

使用union { double d; char a[8]; } union { double d; char a[8]; } union { double d; char a[8]; }为你的一些variables。 通过做一个狭窄的存储(或读 – 修改 – 写)到一个字节导致存储转发失速 。 (该wiki文章还涵盖了许多其他微架构的负载/存储队列)。 例如在高位字节上使用异或0x80来翻转double精度型的符号 ,而不是使用-运算符。 恶魔般无能的开发者可能已经听说FP比整数慢,因此尽量使用整数操作来尽可能地做。 (一个在SSE寄存器中定位FPmath的非常好的编译器可能会把它编译成另一个xmm寄存器中的一个常量xorps ,但是这对x87来说不是太糟糕的唯一方法是如果编译器意识到它是否定值并replace然后加上一个减法。)


如果使用-O3编译而不使用std::atomic ,则强制编译器实际存储/重新加载所有地方。 全局variables(而不是局部variables)也会强制一些存储/重载,但是C ++内存模型的弱sorting并不需要编译器一直向内存溢出/重载。

将本地variablesreplace为一个大结构的成员,这样就可以控制内存布局。

在结构中使用数组来填充(并存储随机数,以certificate它们的存在)。

select你的内存布局,这样一切进入一级caching中相同的“设置”不同的行 。 它只有8路联动,即每路有8个“路”。 caching行是64B。

更好的办法是将事物分开4096B,因为加载对于不同页面的商店存在错误的依赖关系,但是页面内的偏移量相同 。 激进的乱序CPU使用内存消歧来确定加载和存储何时可以重新sorting,而不会改变结果 ,英特尔的实施有误报,阻止负载提前启动。 可能它们只检查页面偏移量以下的位,所以在TLB将高位从虚拟页面转换为物理页面之前可以开始检查。 除了Agner的指导,请参阅Stephen Canon的回答 ,以及接近@Krazy Glew对同一问题的回答。 (Andy Glew是Intel最初的P6微架构的devise师之一)。

使用__attribute__((packed))可以使variables错误alignment,以便跨越caching行甚至页面边界。 (所以一个double的加载需要来自两个caching行的数据)。 在任何英特尔i7 uarch中,未alignment的加载都不会受到任何惩罚,除非跨越caching行和页面行。 caching行分裂仍然需要额外的周期 。 Skylake大大降低了页面拆分负载的罚款, 从100到5个周期。 (第2.1.3节) 。 也许与能够做两页并行的相关。

atomic<uint64_t>上的页面拆分应该是最糟糕的情况 ,尤其是, 如果它是一个页面中的5个字节,另一个页面中是3个字节,或者是4:4以外的任何其他页面。 即使在中间分裂,对于在一些初始阶段(IIRC)的具有16B向量的高速caching行分割也是更有效的。 把所有东西放在一个alignas(4096) struct __attribute((packed)) (当然是为了节省空间),包括一个用于存储RNG结果的数组。 通过在计数器之前使用uint8_tuint16_t来实现错位。

如果你可以让编译器使用索引寻址模式,那将会失败uop微融合 。 也许通过使用#define s来replace简单的标量variablesmy_data[constant]

如果你可以引入一个额外的间接级别,那么加载/存储地址不是很早就知道的,那可以进一步的悲观化。


以非连续的顺序遍历数组

我想我们可以拿出一个无能为力的理由来介绍一个数组:它让我们把随机数的产生与随机数的使用分离开来。 每次迭代的结果也可以存储在一个数组中,稍后加总(具有更多的恶意无能)。

对于“最大随机性”,我们可以让一个线程遍历随机数组写入新的随机数。 消耗随机数的线程可以生成一个随机索引来加载一个随机数。 (这里有一些make-work,但微架构上它有助于加载地址早被发现,所以任何可能的加载延迟都可以在需要加载的数据之前被解决。)在不同内核上具有读写器会导致内存sorting错误 – pipe道清理(如前面讨论的虚假分享案例)。

为了最大程度地保持悲观,用4096字节(即512个双倍)的跨度循环数组。 例如

 for (int i=0 ; i<512; i++) for (int j=i ; j<UPPER_BOUND ; j+=512) monte_carlo_step(rng_array[j]); 

所以访问模式是0,4096,8192,…,
8,4104,8200,…
16,4112,8208,…

这是你得到的像double rng_array[MAX_ROWS][512]的错误顺序(循环遍历行,而不是内部循环中的行内的列,如@JesperJuhl所build议的那样)访问二维数组。 如果恶魔无能可以certificate这样一个尺寸的二维arrays,花园变化的现实世界的无能就容易certificate循环与错误的访问模式。 这发生在现实生活中的真实代码。

如果需要使用多个不同的页面,而不是重复使用相同的几页,则调整循环边界,如果数组不是很大的话。 硬件预取在页面之间也不起作用。 预取程序可以跟踪每个页面中的一个向前和一个向后的stream(这里就是这样),但是只有当内存带宽还没有被非预取饱和时才会对其进行操作。

这也会产生大量的TLB缺失,除非这些页面被合并到一个巨大的页面( Linux对于使用mmap(MAP_ANONYMOUS) )的malloc / new这样的匿名(而不是文件支持)分配mmap(MAP_ANONYMOUS)

而不是一个数组来存储结果列表,你可以使用一个链表 。 然后,每次迭代都需要一个追踪指针的负载(下一个负载的负载地址的RAW真实依赖危险)。 使用错误的分配器,您可能会设法分散内存中的列表节点,导致caching失败。 用一个恶魔无能的分配器,它可以把每个节点放在自己页面的开头。 (例如直接使用mmap(MAP_ANONYMOUS)分配,而不mmap(MAP_ANONYMOUS)页面或跟踪对象大小以正确支持free )。


这些并不是真正的微体系结构特有的,与stream水线没什么关系(其中大部分也是非stream水线CPU的放缓)。

有点离题:让编译器生成更糟糕的代码/做更多的工作:

使用C ++ 11 std::atomic<int>std::atomic<double>获得最美观的代码。 即使没有来自其他线程的争用,MFENCE和lock指令也非常缓慢。

-m32会使代码变慢,因为x87代码会比SSE2代码差。 基于堆栈的32位调用约定需要更多指令,甚至可以将堆栈上的FPparameter passing给exp()等函数。 atomic<uint64_t>::operator++-m32需要lock cmpxchg8B循环 (i586)。 (所以用循环计数器![邪恶的笑声])。

-march=i386也会悲观(谢谢@Jesper)。 FP与fcom比686 fcomi慢。 Pre-586不提供primefaces64位存储(更不用说cmpxchg),所以所有64位atomic操作都编译为libgcc函数调用(可能为i686编译,而不是实际使用锁)。 试试最后一段中的Godbolt编译器资源pipe理器链接。

使用long double / sqrtl / sqrtl额外的精度,并且在sizeof( long double )为10或16(使用填充进行alignment)的ABI中使用额外的慢度。 (IIRC,64位Windows使用8byte long double等价于double (总之,10byte(80bit)FP操作数的加载/存储是4/7 uops,相对于fld m64/m32 / fst, floatdouble只占用1uop) 。即使对于gcc -m64 -march=haswell -O3强制x87和long double也会自动向量化。

如果不使用atomic<uint64_t>循环计数器,则对所有内容使用long double ,包括循环计数器。

atomic<double>编译,但读取 – 修改 – 写入操作(如+=不受支持(甚至在64位上)。 atomic<long double>必须为primefaces加载/存储调用一个库函数。 这可能是非常低效的, 因为x86 ISA本身并不支持primefaces10字节的加载/存储 ,而我可以想象的没有locking的唯一方式( cmpxchg16b )需要64位模式。


-O0 ,通过将零件分配给临时variables来分解一个大表情将会导致更多的存储/重新加载。 没有volatile东西,这与真实代码的实际构build将使用的优化设置无关。

C别名规则允许一个char别名,所以通过char*存储强制编译器在字节存储之前/之后存储/重载所有内容,甚至在-O3 。 (例如,对于在uint8_t数组上运行的自动向量化代码,这是一个问题。)

尝试uint16_t循环计数器,强制截断到16位,可能通过使用16位操作数大小(潜在的摊位)和/或额外movzx指令(安全)。 签名溢出是未定义的行为 ,因此除非使用-fwrapv或至less-fno-strict-overflow , 否则即使用作64位指针的偏移量, 签名的循环计数器也不必在每次迭代时重新签名扩展 。


强制从整数转换为float并再次返回。 和/或double <=> float转换。 指令有超过一个的延迟,而标量int-> float( cvtsi2ss )的devise很糟糕,不能将xmm寄存器的其余部分归零。 (因为这个原因,gcc插入一个额外的pxor来打破依赖关系。)


经常将CPU亲和力设置为不同的CPU (由@Egworbuild议)。 恶魔推理:你不希望一个核心长时间的运行你的线程过热,你呢? 也许交换到另一个核心将使核心涡轮更高的时钟速度。 (事实上​​,它们彼此之间的热量很接近,除了多sockets系统外,这是非常不可能的)。 现在只是调整错误,并经常这样做。 除了在OS保存/恢复线程状态中花费的时间之外,新内核还有冷L2 / L1caching,uopcaching和分支预测器。

频繁引入不必要的系统调用会使你放慢速度,不pipe它们是什么。 尽pipegettimeofday等一些重要而简单的方法可能会在用户空间中实现,而不会转换到内核模式。 (Linux上的glibc会在内核的帮助下执行此操作,因为内核会在vdso导出代码)。

有关系统调用开销(包括返回到用户空间后的caching/ TLB丢失,而不仅仅是上下文切换本身)的更多信息, FlexSC论文对现状进行了一些很好的性能分析,并提供了批处理系统来自大规模multithreading服务器进程的调用。

你可以做的一些事情,使事情尽可能坏:

  • 编译i386架构的代码。 这将防止使用SSE和更新的指令,并强制使用x87 FPU。

  • 无处不在使用std::atomicvariables。 这将使得它们非常昂贵,因为编译器被迫在所有地方插入内存障碍。 而这是一个无能的人可能合理地做“确保线程安全”。

  • 确保以预读程序预测的最糟糕的方式访问内存(列主要vs行主要)。

  • 为了使你的variables更加昂贵,你可以通过为它们分配new而不是让它们具有“自动存储持续时间”(堆栈分配)来确保它们都具有“dynamic存储持续时间”(堆分配)。

注意:这个答案基本上只是总结了我的评论,彼得·科尔德斯已经被纳入他的非常好的答案。 build议他得到你的upvote,如果你只有一个备用:)

你可以使用long double进行计算。 在x86上,它应该是80位格式。 只有传统的x87 FPU才支持这一点。

x87 FPU的几个缺点:

  1. 缺乏SIMD,可能需要更多指导。
  2. 基于堆栈,超级标量和stream水线架构存在问题。
  3. 单独和相当小的一组寄存器,可能需要从其他寄存器和更多的存储器操作更多的转换。
  4. Core i7上有3个SSE端口,x87上只有2个,处理器可以执行更less的并行指令。

Late answer but I don't feel we have abused linked lists and the TLB enough.

Use mmap to allocate your nodes, such that your mostly use the MSB of the address. This should result in long TLB lookup chains, a page is 12 bits, leaving 52 bits for the translation, or around 5 levels it must travers each time. With a bit of luck they must go to memory each time for 5 levels lookup plus 1 memory access to get to your node, the top level will most likely be in cache somewhere, so we can hope for 5*memory access. Place the node so that is strides the worst border so that reading the next pointer would cause another 3-4 translation lookups. This might also totally wreck the cache due to the massive amount of translation lookups. Also the size of the virtual tables might cause most of the user data to be paged to disk for extra time.

When reading from the single linked list, make sure to read from the start of the list each time to cause maximum delay in reading a single number.