关于c ++:取消优化Intel Sandybridge系列CPU中管道的程序

Deoptimizing a program for the pipeline in Intel Sandybridge-family CPUs

为了完成这项任务,我绞尽脑汁已经一个星期了,我希望这里有人能引导我走上正确的道路。让我从讲师的指导开始:

Your assignment is the opposite of our first lab assignment, which was to optimize a prime number program. Your purpose in this assignment is to pessimize the program, i.e. make it run slower. Both of these are CPU-intensive programs. They take a few seconds to run on our lab PCs. You may not change the algorithm.

To deoptimize the program, use your knowledge of how the Intel i7 pipeline operates. Imagine ways to re-order instruction paths to introduce WAR, RAW, and other hazards. Think of ways to minimize the effectiveness of the cache. Be diabolically incompetent.

这项任务让我们可以选择Whetstone或Monte Carlo程序。缓存有效性注释大多只适用于Whetstone,但我选择了蒙特卡洛模拟程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
// Un-modified baseline for pessimization, as given in the assignment
#include     // 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;
}

我所做的更改似乎将代码运行时间延长了一秒钟,但我不完全确定在不添加代码的情况下可以更改什么来暂停管道。一个指向正确方向的点是非常棒的,我很感激任何回应。

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

重点是:

  • 这是社区学院第二学期的建筑课程(使用亨尼西和帕特森的教科书)。
  • 实验室的计算机有很好的CPU
  • 学生们已经接触了CPUID指令,以及如何确定缓存大小,以及intrinsics和CLFLUSH指令。
  • 允许使用任何编译器选项,内联asm也是如此。
  • 写你自己的平方根算法被宣布是在苍白之外

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

所以这听起来像是任务的目标是让学生重新排序现有的工作,以减少指令级的并行性或类似的事情,但这不是一件坏事,人们已经深入钻研和学习了更多。

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


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

非常酷的作业;比我以前看到的那些要求学生为gcc -O0优化一些代码的作业要好得多,学习了一些在真实代码中无关紧要的技巧。在这种情况下,会要求您了解CPU管道,并使用它指导您的去优化工作,而不仅仅是盲目的猜测。其中最有趣的部分是证明每一次骚扰都是"邪恶的无能",而不是蓄意的恶意。好的。

作业措辞和代码有问题:好的。

此代码的UARCH特定选项是有限的。它不使用任何数组,而且大部分成本是调用exp/log库函数。没有一种明显的方法可以或多或少地实现指令级的并行性,并且循环携带的依赖链非常短。好的。

我希望看到这样一个答案:试图通过重新安排表达式来更改依赖项,减少依赖项(危险)带来的ILP,从而减缓速度。我没试过。好的。

Intel SandyBridge系列CPU是一款极具侵略性的无序设计,它使用大量的晶体管和电源来查找并行性,并避免危害(依赖性),而这些危害(依赖性)会给经典的RISC有序流水线带来麻烦。通常只有原始的"真正的"依赖性才是降低速度的传统风险,这些依赖性会导致吞吐量受到延迟的限制。好的。

由于寄存器的重命名,对寄存器的战争和waw危害几乎不是问题。(除popcnt/lzcnt/tzcnt外,它们的目的地对Intel CPU有错误的依赖性,即使它是只写的。也就是说,WAW被当作一个原始的危险+一个写)。对于内存排序,现代CPU使用存储队列将提交延迟到缓存中,直到退役,同时也避免了战争和WAW危险。好的。

为什么muls在haswell上只需要3个周期,不同于agner的指令表?在fp点产品循环中有更多关于寄存器重命名和隐藏fma延迟的信息。好的。

"i7"品牌是由nehalem(core2的继承者)引入的,一些英特尔手册甚至在它们似乎是nehalem时说"corei7",但它们保留了sandybridge和后来的微体系结构的"i7"品牌。snb是当p6家族进化成一个新物种时,snb家族。在许多方面,Nehalem与Pentium III的共同点比SandyBridge的多(例如,寄存器读取暂停和rob读取暂停不会发生在snb上,因为它改为使用物理寄存器文件)。还有一个UOP缓存和一个不同的内部UOP格式)。术语"i7体系结构"并不有用,因为用nehalem而不是core2将snb家族分组是没有意义的。尽管如此,Nehalem还是引入了用于将多个核心连接在一起的共享包容性L3缓存体系结构。以及集成的GPU。所以芯片级别,命名更合理。)好的。总结那些邪恶的无能可以证明的好想法

即使是恶魔般的无能也不可能增加明显的无用的工作或无限循环,而使C++/Boost类的混乱超出了任务的范围。好的。

  • 多线程,带有一个共享的std::atomic循环计数器,因此会发生正确的迭代总数。原子uint64_t对-m32 -march=i586特别不利。对于奖励点数,请安排其不对齐,并使用不均匀的拆分(不是4:4)跨越页面边界。
  • 其他一些非原子变量的错误共享->内存顺序错误推测管道清除,以及额外的缓存未命中。
  • XOR不是在fp变量上使用-,而是在高位字节使用0x80来翻转符号位,从而导致存储转发暂停。
  • 单独计时每次迭代,甚至比RDTSC更重的东西。例如,CPUID/RDTSC或发出系统调用的时间函数。串行化指令天生就不友好。
  • 变化乘以常数除以它们的倒数("便于阅读")。DIV速度慢,没有完全安装管道。
  • 使用avx(simd)对multiple/sqrt进行矢量化,但在调用标量数学库exp()log()函数之前未能使用vzeroupper,导致avx<->sse转换暂停。
  • 将RNG输出存储在一个链接列表中,或者存储在您无序遍历的数组中。每次迭代的结果都一样,最后求和。

这个答案也包含了,但不包括在总结中:建议在非流水线的CPU上同样慢,或者即使在恶魔般的无能下也似乎不合理。例如,许多gimp编译器的思想会产生明显不同/更差的asm。好的。多线程严重

也许可以使用OpenMP来多线程循环,迭代次数很少,开销远大于速度增益。不过,您的蒙特卡洛代码有足够的并行性来实际加速,特别是如果我们成功地使每个迭代变慢的话。(每个线程计算一个在末尾添加的部分payoff_sum)。那个循环上的EDOCX1[1]可能是一个优化,而不是一个对峙。好的。

多线程,但强制两个线程共享相同的循环计数器(使用atomic增量,这样迭代总数是正确的)。这似乎是非常合乎逻辑的。这意味着使用static变量作为循环计数器。这就为循环计数器使用atomic提供了理由,并创建了实际的缓存线乒乓(只要线程不在具有超线程的同一物理核心上运行;这可能不会很慢)。不管怎样,这比不争的lock inc案件要慢得多。而在32位系统上,lock cmpxchg8b以原子方式递增一个争用的uint64_t,则必须在一个循环中重试,而不是让硬件仲裁一个原子的inc。好的。

还创建假共享,其中多个线程将其私有数据(例如RNG状态)保存在同一缓存线的不同字节中。(有关它的英特尔教程,包括要查看的性能计数器)。这有一个特定于微体系结构的方面:英特尔CPU推测不会发生内存顺序错误,并且有一个内存顺序机器清除性能事件来检测这一点,至少在P4上。对哈斯韦尔的惩罚可能没有那么大。正如链接指出的那样,locked指令假定会发生这种情况,避免了错误的推测。正常负载推测其他核心不会使在执行负载和按程序顺序退出之间的缓存线失效(除非使用pause)。没有locked指令的真正共享通常是一个错误。将非原子共享循环计数器与原子情况进行比较是很有趣的。要真正地进行混淆,请保留共享的原子循环计数器,并在相同或不同的缓存行中为其他变量导致错误共享。好的。随机UARCH特定想法:

如果您可以引入任何不可预知的分支,这将严重地困扰代码。现代的x86 CPU有相当长的管道,因此预测失误需要大约15个周期(从UOP缓存运行时)。好的。依赖链:

我认为这是任务的一部分。好的。

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

要真正使其有效,请增加循环携带的依赖链的长度。不过,没有什么比这更明显的了:所编写的循环有非常短的循环携带依赖链:只是一个fp加法。(3个周期)。多个迭代可以同时进行计算,因为它们可以在前一个迭代结束时的payoff_sum +=之前开始。(log()exp接受了许多指令,但不比haswell的无序窗口更容易找到并行性:rob大小=192个融合域Uops,调度程序大小=60个未使用的域Uops。一旦当前迭代的执行进展到足以为下一个迭代的指令留出空间来发布,当旧的指令使执行单元空闲时(例如,因为它们在延迟上受到瓶颈,而不是通过p),任何准备好输入的部分(即独立/单独的DEP链)都可以开始执行。U.)好的。

RNG状态几乎肯定会比addps更长的循环承载依赖链。好的。使用较慢/更多的FP操作(尤其是更多的部门):

除以2.0而不是乘以0.5,依此类推。fp multiply在Intel设计中采用了大量流水线,在Haswell和更高版本上每0.5C有一个吞吐量。FP-divsd/divpd只是部分管线。(尽管Skylake对于divpd xmm的每4c吞吐量有一个令人印象深刻的吞吐量,具有13-14c的延迟,而在nehalem(7-22c)上根本没有流水线)。好的。

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)的原因)。参见保罗的评论,了解一个非常复杂的嵌套pow()建议。好的。

如果您可以将计算缩小到非常小的数字,那么当对两个正常数字的操作产生非正常值时,fp math ops需要大约120个额外的周期来陷阱到微码。有关确切的数字和详细信息,请参阅Agner Fog的Microarch PDF。这是不太可能的,因为你有很多乘数,所以比例因子将被平方和下溢一直到0.0。我看不出任何方法可以用无能(甚至是恶毒的)来证明必要的规模扩张是故意的。好的。如果您可以使用intrinsics()

使用movnti将数据从缓存中逐出。恶魔:它是新的,而且顺序很弱,所以应该让CPU运行得更快,对吧?或者,如果有人正处于这样做的危险中(对于只有部分位置是热的分散的写操作),请查看相关的问题。没有恶意,clflush可能是不可能的。好的。

在fp数学运算之间使用整数随机移动以导致绕过延迟。好的。

在没有正确使用vzeroupper的情况下混用SSE和AVX指令会导致天湖前的大隔间(天湖的处罚也不同)。即使没有这一点,矢量化也可能比标量更糟糕(花费更多的周期将数据移入/移出向量,而不是通过一次对4个蒙特卡洛迭代执行add/sub/mul/div/sqrt操作(256b向量)来保存数据)。add/sub/mul执行单元是完全流水线和全宽的,但是256b向量上的div和sqrt不如128b向量(或scalars)上的速度快,因此对于double来说,加速并不显著。好的。

exp()log()没有硬件支持,因此这部分需要将向量元素提取回标量,并分别调用库函数,然后将结果重组为向量。libm通常只编译为使用sse2,因此将使用标量数学指令的遗留sse编码。如果您的代码使用256b向量,并且在不首先执行vzeroupper的情况下调用exp,那么您将暂停。返回后,像vmovsd这样的AVX-128指令也将暂停,以将下一个向量元素设置为exp的arg。然后,当运行SSE指令时,exp()将再次暂停。这正是这个问题中发生的事情,导致了10倍的减速。(谢谢@zboson)。好的。

另请参阅Nathan Kurz对Intel的Math Lib与Glibc的实验,了解此代码。未来的glibc将提供exp()等的矢量化实现。好的。

如果目标是IVB前,特别是Nehalem,请尝试让GCC通过16位或8位操作导致部分寄存器暂停,然后再进行32位或64位操作。在大多数情况下,GCC会在8位或16位操作后使用movzx,但这里有一种情况,GCC修改ah,然后读取ax。好的。使用(内联)ASM:

使用(内联)asm,您可以断开UOP缓存:一个32B的代码块不适合三个6UOP缓存行,它强制从UOP缓存切换到解码器。一个不称职的ALIGN使用多个单字节nop,而不是在内环中的分支目标上使用两个长的nop,可能会起到这种作用。或者将对齐填充放在标签后,而不是之前。:p这只在前端是一个瓶颈时才重要,如果我们成功地纠缠其余的代码,这就不会是瓶颈了。好的。

使用自我修改代码触发管道清除(即机器核武器)。好的。

16位指令的LCP暂停,且立即数太大,无法容纳8位,因此不太可能有用。SNB和以后的UOP缓存意味着您只需支付一次解码惩罚。在Nehalem(第一个i7)上,它可能适用于不适合28uop循环缓冲区的循环。GCC有时会生成这样的指令,即使使用-mtune=intel,并且可以使用32位指令。好的。

定时的一个常见习惯用法是EDOCX1(序列化),然后是EDOCX1(序列化)。用CPUIDRDTSC分别计时每次迭代,以确保RDTSC没有按照早期的指令重新排序,这将大大降低速度。(在现实生活中,明智的时间安排方法是将所有迭代都计时在一起,而不是分别计时并将它们相加)。好的。导致大量的缓存未命中和其他内存下降

对某些变量使用union { double d; char a[8]; }。通过对其中一个字节执行窄存储(或读-修改-写),导致存储转发暂停。(这篇wiki文章还涵盖了许多其他用于加载/存储队列的微体系结构内容)。例如,仅在高字节上使用xor 0x80翻转double的符号,而不是使用-运算符。不称职的开发人员可能听说fp比integer慢,因此尽量使用integer ops。(一个在SSE寄存器中以fp数学为目标的非常好的编译器可能会用另一个xmm寄存器中的一个常量将其编译为xorps,但对x87来说这并不可怕的唯一方法是编译器意识到它正在求反值,并用减法替换下一个加法。)好的。

如果使用-O3编译而不使用std::atomic,则使用volatile,以强制编译器在整个地方实际存储/重新加载。全局变量(而不是本地变量)也会迫使一些存储/重新加载,但是C++内存模型的弱排序并不要求编译器总是将内存溢出/重新加载到内存中。好的。

用一个大结构的成员替换本地变量,这样就可以控制内存布局。好的。

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

选择内存布局,使所有内容都进入一级缓存中相同"集合"中的不同行。它只有8路关联,即每个集合有8路。缓存线为64B。好的。

更好的是,将内容精确地分开4096b,因为加载对不同页面的存储有错误的依赖关系,但在一个页面内具有相同的偏移量。激进的无序CPU使用内存消除来确定什么时候可以在不改变结果的情况下对加载和存储进行重新排序,而Intel的实现具有错误的积极性,可以防止加载过早启动。可能它们只检查页面偏移量以下的位,因此可以在TLB将高位从虚拟页面转换为物理页面之前开始检查。除了《阿格纳指南》,还可以看到斯蒂芬·卡农的答案,以及@krazy glew关于同一问题的答案末尾的一节。(Andy Glew是英特尔最初的p6微体系结构的架构师之一。)好的。

使用__attribute__((packed))可以让您错误地对齐变量,使它们跨越缓存线甚至页面边界。(因此,一个double的负载需要来自两条缓存线的数据)。未对齐的加载在任何Inteli7uarch中都不会受到惩罚,除非在跨越缓存线和页面线时。缓存线拆分仍需要额外的周期。Skylake显著减少了页面分割加载的惩罚,从100到5个周期。(第2.1.3节)。可能与能够同时进行两页的浏览有关。好的。

atomic上的页面拆分应该是最坏的情况,特别是如果一个页面中有5个字节,另一个页面中有3个字节,或者不是4:4。在某些UARCH(IIRC)上,即使从中间拆分,也能更有效地使用16B向量进行缓存线拆分。把所有东西放在一个alignas(4096) struct __attribute((packed))中(当然是为了节省空间),包括一个存储RNG结果的数组。通过在计数器前使用uint8_tuint16_t来实现不对中。好的。

如果你能让编译器使用索引寻址模式,那将击败UOP微融合。可以用#define代替简单的标量变量。好的。

如果您可以引入一个额外级别的间接寻址,那么加载/存储地址就不早知道了,这可能会进一步困扰您。好的。以非连续顺序遍历数组

我认为我们可以首先提出引入数组的不充分理由:它允许我们将随机数生成与随机数使用分开。每一次迭代的结果也可以存储在一个数组中,稍后再进行总结(具有更多的恶魔般的无能)。好的。

对于"最大随机性",我们可以让一个线程在随机数组上循环,向其中写入新的随机数。使用随机数的线程可以生成随机索引以从中加载随机数。(这里有一些工作,但在微体系结构上,它有助于尽早知道加载地址,以便在需要加载的数据之前解决任何可能的加载延迟。)在不同的核心上拥有读写器将导致内存顺序错误推测管道清除(前面为错误共享案例讨论过)。好的。

为了实现最大的混乱,请以4096字节的步幅(即512倍)循环您的数组。例如好的。

1
2
3
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所建议的,循环遍历行,而不是内部循环中的行中的列)。如果恶魔般的不称职可以证明二维数组的尺寸是这样的,那么花园式的现实世界的不称职很容易证明循环使用了错误的访问模式。这发生在现实生活中的真实代码中。好的。

如果数组不太大,则可以根据需要调整循环边界,以使用多个不同的页,而不是重复使用相同的几页。硬件预取在整个页面上都不起作用。预取器可以跟踪每个页面中的一个前向流和一个后向流(这就是这里所发生的情况),但仅当内存带宽尚未被非预取饱和时才会对其执行操作。好的。

这也会产生大量的TLB遗漏,除非页面合并成一个大页面(Linux为匿名(不支持文件)分配(如malloc/new,使用mmap(MAP_ANONYMOUS))。好的。

您可以使用链接列表来代替存储结果列表的数组。然后,每个迭代都需要一个指针跟踪负载(对于下一个负载的负载地址来说,这是一个原始的、真实的依赖性风险)。使用一个坏的分配器,您可能会设法分散内存中的列表节点,从而破坏缓存。使用一个非常不称职的分配器,它可以将每个节点放在自己页面的开头。(例如,直接与mmap(MAP_ANONYMOUS)分配,而不拆分页面或跟踪对象大小,以正确支持free)。好的。

这些并不是特定于微体系结构的,与管道几乎没有关系(其中大部分还可能是非流水线CPU的速度减慢)。好的。稍微偏离主题:使编译器生成更糟糕的代码/做更多的工作:

对于最悲观的代码,使用C++ 11 EDCOX1、6和EDOCX1 7。mfences和locked指令非常慢,即使没有来自另一个线程的争用。好的。

-m32将使代码变慢,因为x87代码将比sse2代码更糟。基于堆栈的32位调用约定需要更多的指令,甚至将堆栈上的fp参数传递给像exp()这样的函数。-m32上的atomic::operator++需要一个lock cmpxchg8B循环(i586)。(所以把它用于循环计数器![邪恶的笑])好的。

-march=i386也会纠缠(谢谢@jesper)。FP与fcom相比慢于686 fcomi。pre-586不提供原子64位存储(更不用说cmpxchg),因此所有64位atomic操作都编译为libgcc函数调用(可能是为i686编译的,而不是实际使用锁)。在最后一段中的godbolt编译器资源管理器链接上试试。好的。

使用long double/方法/ sqrtlexpl超精密和超slowness在Abis在sizeof(long double)是10或16(与PADDING ON中心对齐)。(IIRC,64位的Windows使用的8byte long double当量的double。。。。。。。(不管怎样,加载/存储部(FP 10byte 80bit)operands是4月7 uops,与floatdouble只读或以每1 UOP方法fld m64/m32/ fst)。forcing X87与long doubledefeats自动矢量化的方法-m64 -march=haswell -O3夏娃的海湾。

/好的。

如果不使用atomic环counters,long double使用一切方法,包括counters回路。

/好的。

atomiccompiles,但读-修改-写操作的类+=不是T载体的方法(即使它是64位)。atomichas to a库函数的呼叫中心是原子loads /商店。它的probably真的很低,因为x86 ISA服务器不支持naturally T原子10byte loads /店,和唯一的办法,我可以认为大学不自锁(cmpxchg16b)requires 64位模式。

/好的。

-O0破,弹出的一个大的表达被分配到临时track vars配件会因为更多的商店/ reloads。。。。。。。没有volatile或东西,这不会物与优化的设置,建立一个真正的实时代码会使用。

/好的。

C混叠的规则允许的任何一char别名,即储char*兵的一通编译器的商店/ reload之前,一切都在一个字节/商店,甚至在-O3。。。。。。。(这是一个解决问题的方法的自动矢量化的代码,这是一operates阵列uint8_t,for example)。

/好的。

试着uint16_t环counters,到部队截断到16位,probably通过使用16位operand尺寸(档位)和/或额外的movzx指示(安全)。签署了OVERFLOW是undefined行为,所以除非你使用-fwrapv或至少-fno-strict-overflow,环签名counters没有计划将重新登录,每一个迭代扩展,甚至如果使用AS offsets 64位的分球。

/好的。

力的变换从整数到float和倒了。和/或double< = > floatconversions。。。。。。。指示要为比一个潜伏期,和标量int -> Float(cvtsi2ss)是badly自行设计的非零的休息的XMM寄存器。海湾合作委员会(GCC inserts安pxor额外的休息dependencies设计的,这的原因。)

/好的。

你的CPU的仿射竞相集到一个不同的CPU(suggested主编的"egwor)。diabolical推理:你不想要一个核to get overheated从运行你的线程,一个长的时间,你呢?也许swapping到另一个核,核汽轮会让我们的更高的时钟速度。(在现实:他们是thermally SO CLOSE到每一其他,这是高unlikely除了在一个多端口系统)。现在我得到的错误校正和做通太常常准备。besides的时间都在操作系统的节电/恢复线程状态,新的核有冷的L2和L1 caches UOP,高速缓存,分支和predictors。。。。。。。

/好的。

频繁地引入不必要的系统调用,无论它们是什么,都会减慢您的速度。虽然有些重要但简单的方法,如gettimeofday可以在用户空间中实现,但不能过渡到内核模式。(Linux上的glibc在内核的帮助下完成这项工作,因为内核在vdso中导出代码)。好的。

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


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

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

  • 在任何地方都使用std::atomic变量。这将使它们非常昂贵,因为编译器被迫在各处插入内存屏障。这是一个不称职的人为"确保线程安全"可能会做的事情。

  • 确保以最差的方式访问内存,以便预取器进行预测(列主控与行主控)。

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

  • 确保您分配的所有内存都是非常奇怪的对齐方式,并且无论如何都要避免分配巨大的页面,因为这样做会非常高效。

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

注意:这个答案基本上只是总结了我的评论,@peter cordes已经融入了他的好答案中。如果你只有一张备用票,建议他投你的赞成票。)


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

X87 FPU的几个缺点:

  • 缺少SIMD,可能需要更多指令。
  • 基于堆栈,对于超标量和流水线结构有问题。
  • 单独的和非常小的寄存器集,可能需要从其他寄存器进行更多的转换和更多的内存操作。
  • 在核心I7上,SSE有3个端口,X87只有2个端口,处理器可以执行较少的并行指令。

  • 回答晚了,但我觉得我们滥用链接列表和TLB还不够。

    使用mmap来分配您的节点,这样您就主要使用地址的msb。这将导致长的TLB查找链,一个页面是12位,留下52位用于翻译,或者每次必须遍历5个级别。幸运的是,它们每次都必须进入内存进行5级查找,再加上1个内存访问才能到达您的节点,顶层很可能位于缓存中的某个位置,因此我们希望能够访问5*内存。放置节点,使其跨过最差的边界,以便读取下一个指针将导致另一个3-4个翻译查找。由于大量的翻译查找,这也可能完全破坏缓存。另外,虚拟表的大小可能会导致大多数用户数据被分页到磁盘上以获得额外的时间。

    从单个链接列表中读取时,请确保每次都从列表的开头读取,以使读取单个数字的最大延迟。