英特尔Sandybridge的管道优化项目

我一直在努力完成这项任务,花了一个星期的时间,我希望有人能带领我走向正确的道路。 让我从讲师的指示开始:

您的任务与我们的第一个实验任务相反,这是为了优化质数计划。 你在这项任务中的目的是让程序变得更加悲观,即让它运行得更慢。 这两个都是CPU密集型程序。 他们需要几秒钟才能在我们的实验室PC上运行。 你可能不会改变算法。

为了消除该方案的优化,请使用您对英特尔i7管道如何运作的了解。 想象一下如何重新排列指令路径来引入WAR,RAW和其他危险。 想想如何最大限度地减少缓存的有效性。 恶魔般无能。

这项任务给了Whetstone或Monte-Carlo项目的选择。 缓存效率评论大多只适用于Whetstone,但我选择了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;
}

我所做的更改似乎将代码运行时间增加了一秒,但我并不完全确定可以在不添加代码的情况下停止管道。 指向正确的方向会很棒,我很欣赏任何回应。


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

亮点是:

  • 这是社区学院第二学期的建筑课程(使用Hennessy和Patterson教科书)。
  • 实验室计算机具有Haswell CPU
  • 学生们已经接触到CPUID指令以及如何确定缓存大小,以及内部函数和CLFLUSH指令。
  • 任何编译器选项都是允许的,并且内联asm也是如此。
  • 编写自己的平方根算法被宣布为不在脸色苍白
  • Cowmoogun对元线程的评论表明,并不清楚编译器优化可能是其中的一部分,并假设为-O0 ,运行时间增加17%是合理的。

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


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


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

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


    转让措辞和代码存在问题

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

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

    英特尔Sandybridge系列CPU是积极的乱序设计,它们花费大量晶体管和电源来寻找并行性并避免危害经典RISC有序流水线的危险(依赖性)。 通常,减慢速度的唯一传统危险是导致吞吐量受延迟限制的RAW“真实”依赖性。

    由于寄存器重命名,寄存器的WAR和WAW危害几乎不是问题 。 (除了popcnt / lzcnt / tzcnt ,它们对Intel CPU的目的地具有错误的依赖关系,即使它是只写的也就是说,WAW被作为RAW危险+写入)。 对于内存排序,现代CPU使用存储队列将延迟提交到缓存直到退役,同时避免WAR和WAW危害。

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


    恶魔无能可以证明的好想法总结

    即使是恶魔般的无能也不会增加明显无用的工作或无限循环,并且使C ++ / Boost类变得混乱超出了赋值范围。

  • 多线程与单个共享std::atomic<uint64_t>循环计数器,因此正确的迭代次数发生。 原子uint64_t在-m32 -march=i586特别糟糕。 对于奖励积分,安排它不对齐,并跨越不均匀分割的页面边界(而不是4:4)。
  • 假分享其他一些非原子变量 - >内存顺序错误猜测管道清除,以及额外的缓存未命中。
  • 而不是使用-在FP变量,XOR具有0x80的高字节翻转符号位,导致存储-转发摊位
  • 每次迭代的时间都是独立的,比RDTSC还要重。 例如CPUID / RDTSC或进行系统调用的时间函数。 序列化指令本质上是管道不友好的。
  • 改变乘常数除以它们的倒数(“便于阅读”)。 div很慢并且没有完全流水线。
  • 使用AVX(SIMD)矢量化multiply / sqrt,但在调用标量数学库exp()log()函数之前未使用vzeroupper ,导致AVX < - > SSE转换失速
  • 将RNG输出存储在链接列表中,或存储在不按顺序遍历的数组中。 每次迭代的结果都是一样的,最后总和。
  • 这个答案也包括在内,但不包括在总结中:对于非流水线CPU来说,这些建议可能会很慢,或者即使在恶意无能的情况下,这些建议似乎也不合理。 例如许多gimp-the-compiler的想法会产生明显不同的/更糟糕的东西。


    多线程严重

    也许使用OpenMP进行多线程循环的迭代次数非常少,其开销比速度增加更多。 尽管如此,你的蒙特卡洛代码具有足够的并行性来实现加速。 如果我们成功地使每次迭代变得缓慢。 (每个线程计算一个部分payoff_sum ,在最后添加)。 在该循环上#omp parallel可能是一个优化,而不是悲观。

    多线程但强制两个线程共享相同的循环计数器(使用atomic增量,因此迭代的总数是正确的)。 这似乎是恶毒的逻辑。 这意味着使用一个static变量作为循环计数器。 这证明了循环计数器使用atomic ,并创建实际的高速缓存行乒乓(只要线程不在具有超线程的同一物理内核上运行;可能不会太慢​​)。 无论如何,这比lock inc的无争议案件要慢得多。 并且lock cmpxchg8b以在32位系统上原子地增加争用的uint64_t将不得不在循环中重试,而不是硬件仲裁原子inc

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


    随机uarch特定的想法:

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


    依赖链:

    我认为这是该任务的预期部分之一。

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

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

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


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

    除以2.0而不是乘以0.5,依此类推。 在英特尔设计中,FP乘法非常流水,在Haswell和以后的版本中每0.5c的吞吐量就有一个。 FP divsd / divpd只是部分流水线 。 (虽然Skylake的每4c吞吐量令人印象深刻,但是在Nehalem(7-22c)上,对于divpd xmm ,延迟为divpd xmm ,而不是流水线。

    do { ...; euclid_sq = x*x + y*y; } while (euclid_sq >= 1.0); do { ...; euclid_sq = x*x + y*y; } while (euclid_sq >= 1.0); 显然是测试一段距离,所以显然它适合sqrt()它。 :P( sqrtdiv更慢)。

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

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


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

    使用movnti从缓存中movnti数据。 Diabolical:它是新的和弱有序的,所以应该让CPU更快地运行它,对吧? 或者看到一个关联的问题,即某个人有可能做到这一点(对于只有一些位置很热的散文写作)。 没有恶意, clflush可能是不可能的。

    在FP数学运算之间使用整数混洗来产生旁路延迟。

    在没有正确使用vzeroupper情况下混合SSE和AVX指令会在Skylake之前造成较大的摊位 (并且在Skylake中会有不同的处罚)。 即使没有这一点,向量化严重可能比标量更糟糕(通过对256个向量一次执行4次蒙特卡罗迭代的add / sub / mul / div / sqrt操作节省了数据进入/离开向量所花费的更多周期) 。 add / sub / mul执行单元是完全流水线和全宽的,但256b向量上的div和sqrt并不像128b向量(或标量)那样快,所以加速对于double来说并不显着。

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

    另请参阅Nathan Kurz关于此代码的英特尔数学库与glibc的实验。 未来的glibc将带有exp()等矢量化实现。


    如果瞄准pre-IvB,或者特别 Nehalem尝试让gcc使用16位或8位操作导致部分寄存器停顿,接着进行32位或64位操作。 在大多数情况下,gcc在8或16位操作之后会使用movzx ,但是这里有一个情况,gcc修改ah然后读取ax


    With(inline)asm:

    使用(内联)asm,您可能会破坏uop缓存:不适合三个6uop缓存行的32B代码块会强制从uop缓存切换到解码器。 在内部循环内的分支目标上使用许多单字节的nop而不是长的nop可能会导致无能的ALIGN 。 或者将对齐填充放在标签之后,而不是之前。 :P这只在前端是瓶颈时才重要,如果我们成功地对其余代码进行了悲观化,那么这将不会成为问题。

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

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


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


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

    使用union { double d; char a[8]; } union { double d; char a[8]; } union { double d; char a[8]; }为你的一些变量。 通过对一个字节进行窄存储(或读 - 修改 - 写) 导致存储转发失速 。 (这篇wiki文章还涵盖了许多其他适用于加载/存储队列的微体系结构)。 例如在高位字节上使用异或0x80来翻转double精度型的符号 ,而不是使用-运算符。 恶魔般无能的开发人员可能听说FP比整数慢,因此尽量使用整数操作来尽可能地做到这一点。 (一个非常好的针对SSE寄存器中的FP数学的编译器可能会将其编译为另一个xmm寄存器中的常量xorps ,但唯一的办法是x87不会太糟糕,因为编译器意识到它会否定值并替换接下来添加一个减法。)


    如果使用-O3编译并且不使用std::atomic ,那么使用volatile来强制编译器实际存储/重新加载整个地方。 全局变量(而不是局部变量)也会强制某些存储/重载,但C ++内存模型的弱排序并不要求编译器始终将内存溢出/重新加载到内存中。

    将局部变量替换为大结构的成员,以便可以控制内存布局。

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

    选择你的内存布局,这样一切都进入L1缓存中相同“设置”的不同行。 它只有8路联合,即每组有8个“路径”。 缓存行是64B。

    更好的是, 把事情完全分开4096B,因为加载对不同页面的商店有一个错误的依赖关系,但是页面内的偏移量相同 。 积极的乱序CPU使用Memory Disambiguation(内存消歧)来确定加载和存储何时可以在不改变结果的情况下进行重新排序,并且英特尔的实施具有误报,以防止负载提前启动。 可能它们只检查页面偏移量以下的位,因此可以在TLB将高位从虚拟页面转换为物理页面之前开始检查。 除了Agner的指导,请参阅Stephen Canon的回答,以及@Krazy Glew关于同一问题的答案的结尾部分。 (Andy Glew是英特尔原创P6微架构的设计师之一。)

    使用__attribute__((packed))可以让变量错误对齐,从而跨越缓存行甚至页面边界。 (因此,一个double的加载需要来自两个缓存行的数据)。 在任何英特尔i7 uarch中,未对齐的负载均不会受到惩罚,除非跨越缓存线和页面线。 缓存行分裂仍然需要额外的周期。 Skylake大大降低了页面拆分负载的罚款,从100到5个周期。 (第2.1.3节)。 也许与能够并行进行两页分页有关。

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

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

    如果您可以引入额外的间接级别,那么加载/存储地址不会提前知道,这可能会进一步恶化。


    以非连续顺序遍历数组

    我想我们可以拿出无能为力的理由来引入一个数组:它让我们将随机数的生成与随机数的使用区分开来。 每次迭代的结果也可以存储在一个数组中,稍后求和(更多的恶魔无能)。

    对于“最大随机性”,我们可以让一个线程循环遍历随机数组,将新的随机数写入它。 消耗随机数的线程可以生成一个随机索引来加载一个随机数。 (这里有一些制作工作,但是微架构上它有助于加载地址更早地知道,因此任何可能的加载延迟都可以在需要加载数据之前解决。)在不同内核上安装读写器会导致内存排序错误 - 猜测管道清除(如前面讨论的虚假分享案例)。

    为了达到最大程度的悲观化,用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所建议的循环内部循环中的行)。 如果恶魔无能可以证明具有这种尺寸的2D阵列是合理的,那么花园种类的真实世界无能就容易证明以错误的访问模式循环。 这发生在现实生活中的真实代码中。

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

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

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


    这些并不是真正的微体系结构特定的,并且与流水线无关(其中大部分还会导致非流水线CPU的减速)。

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

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

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

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

    使用long double / sqrtl / expl额外精度和额外的缓慢中的ABI其中的sizeof( long double )为10或16(具有填充用于对准)。 (IIRC,64位Windows使用8字节long double相当于double 。(反正10byte(80bit的)FP操作数加载/存储是4/7微指令,相对于floatdouble每个仅服用1 UOP fld m64/m32 / fst ) 。即使对于gcc -m64 -march=haswell -O3强制x87与long double也会失败自动向量化。

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

    atomic<double>编译,但不支持像+=这样的读取 - 修改 - 写入操作(即使在64位上)。 atomic<long double>必须为原子加载/存储调用一个库函数。 这可能效率很低,因为x86 ISA本身并不支持10byte的原子加载/存储,而且我可以不用锁定的唯一方式( cmpxchg16b )就需要64位模式。


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

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

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


    强制从整数转换为float并再次返回。 和/或double <=> float转换。 指令的延迟大于1,并且标量int-> float( cvtsi2ss )设计得非常糟糕,无法将xmm寄存器的其余部分归零。 (出于这个原因,gcc插入一个额外的pxor来打破依赖关系。)


    经常将CPU亲和力设置为不同的CPU (由@Egwor建议)。 恶魔推理:你不希望一个核心长时间运行你的线程过热,你呢? 也许交换到另一个核心将让核心涡轮增加到更高的时钟速度。 (事实上​​:它们彼此之间的热量很接近,除了多插座系统外,这种情况极少发生)。 现在只需调整错误,并且经常这样做。 除了在OS保存/恢复线程状态中花费的时间之外,新内核还有冷L2 / L1缓存,uop缓存和分支预测器。

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

    有关系统调用开销(包括返回到用户空间后的缓存/ TLB未命中,而不仅仅是上下文切换本身)的更多信息,FlexSC论文对当前情况进行了一些很好的性能分析,并提供了批处理系统来自大量多线程服务器进程的调用。


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

  • 编译i386体系结构的代码。 这将阻止使用SSE和更新的指令并强制使用x87 FPU。

  • 无处不在使用std::atomic变量。 由于编译器被迫在整个地方插入内存屏障,这将使它们非常昂贵。 这是一个无能的人可能合理地做的事情,以“确保线程安全”。

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

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

  • 确保你分配的所有内存非常奇怪地排列,并且通过一切手段避免分配巨大的页面,因为这样做会大大提高TLB的效率。

  • 无论你做什么,都不要在启用编译器优化器的情况下构建代码。 并确保启用最具表现力的调试符号(不会使代码运行速度变慢,但会浪费一些额外的磁盘空间)。

  • 注意:这个答案基本上总结了我的评论,即@Peter Cordes已经将其纳入他的非常好的答案中。 建议如果你只有一个备用的话,他会得到你的upvote :)


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

    x87 FPU的几个缺点:

  • 缺少SIMD,可能需要更多说明。
  • 基于堆栈,超级标量和流水线架构存在问题。
  • 单独和相当小的一组寄存器,可能需要更多的从其他寄存器转换和更多的存储器操作。
  • 在酷睿i7上有3个SSE端口和2个x87,处理器可以执行更少的并行指令。
  • 链接地址: http://www.djcxy.com/p/72565.html

    上一篇: Deoptimizing a program for the pipeline in Intel Sandybridge

    下一篇: multiplication using SSE (x*x*x)