我希望我将问题简化为一个简单且可重复的测试用例.源(在此处)包含10个相同的简单循环副本.每个循环的形式如下:
#define COUNT (1000 * 1000 * 1000)
volatile uint64_t counter = 0;
void loopN(void) {
for (int j = COUNT; j != 0; j--) {
uint64_t val = counter;
val = val + 1;
counter = val;
}
return;
}
Run Code Online (Sandbox Code Playgroud)
变量的'volatile'很重要,因为它强制值在每次迭代时从内存中读取和写入.使用'-falign-loops = 64'将每个循环对齐到64个字节,并生成相同的程序集,除了对全局的相对偏移量:
400880: 48 8b 15 c1 07 20 00 mov 0x2007c1(%rip),%rdx # 601048 <counter>
400887: 48 83 c2 01 add $0x1,%rdx
40088b: 83 e8 01 sub $0x1,%eax
40088e: 48 89 15 b3 07 20 00 …Run Code Online (Sandbox Code Playgroud) 我已经意识到Little's Law限制了在给定的延迟和给定的并发级别下数据传输的速度.如果您想更快地传输某些内容,则需要更大的传输,更多的"飞行中"传输或更低的延迟.对于从RAM读取的情况,并发性受到行填充缓冲区数量的限制.
当加载错过L1缓存时,将分配行填充缓冲区.现代英特尔芯片(Nehalem,Sandy Bridge,Ivy Bridge,Haswell)每个核心有10个LFB,因此每个核心限制为10个未完成的缓存未命中.如果RAM延迟为70 ns(似乎合理),并且每次传输为128字节(64B高速缓存线加上其硬件预取双线),则将每个内核的带宽限制为:10*128B/75 ns = ~16 GB/s.诸如单线程Stream之类的基准确认这是相当准确的.
减少延迟的显而易见的方法是使用x64指令(如PREFETCHT0,PREFETCHT1,PREFETCHT2或PREFETCHNTA)预取所需数据,这样就不必从RAM中读取数据.但是我无法通过使用它们加快速度.问题似乎是__mm_prefetch()指令本身消耗LFB,因此它们也受到相同的限制.硬件预取不会触及LFB,也不会跨越页面边界.
但我无法在任何地方找到任何记录.我发现的最接近的是15年前的文章,其中提到Pentium III上的预取使用Line Fill Buffers.我担心事情可能会发生变化.由于我认为LFB与L1缓存相关联,我不确定为什么L2或L3的预取会消耗它们.然而,我测量的速度与这种情况一致.
那么:有没有办法在没有使用这10个线路填充缓冲器中的一个的情况下从存储器中的新位置开始取出,从而通过绕过Little's定律实现更高的带宽?
我正在尝试加速可变位宽整数压缩方案,我有兴趣在运行中生成和执行汇编代码.目前,大量时间花在错误预测的间接分支上,并且基于所发现的一系列位宽生成代码似乎是避免这种惩罚的唯一方法.
一般技术被称为"子程序线程"(或"调用线程",尽管这也有其他定义).目标是利用处理器有效的呼叫/返回预测,以避免停顿.这种方法在这里有详细描述:http: //webdocs.cs.ualberta.ca/~amaral/cascon/CDP05/slides/CDP05-berndl.pdf
生成的代码只是一系列调用,后跟返回.如果有5个"块"的宽度[4,8,8,4,16],它看起来像:
call $decode_4
call $decode_8
call $decode_8
call $decode_4
call $decode_16
ret
Run Code Online (Sandbox Code Playgroud)
在实际使用中,它将是一个较长的一系列调用,具有足够的长度,每个系列可能是唯一的,只调用一次.无论是在这里还是在其他地方,生成和调用代码都有详细记录.但是,除了简单的"不要做"或者考虑周全的"有龙"之外,我还没有找到很多关于效率的讨论.即使是英特尔文档也主要说明了一般性:
8.1.3处理自我修改和交叉修改代码
处理器将数据写入当前正在执行的代码段以将该数据作为代码执行的动作称为自修改代码.IA-32处理器在执行自修改代码时表现出特定于模型的行为,具体取决于当前执行指针在代码被修改之前的距离....自修改代码将以比非自修改或普通代码更低的性能级别执行.性能恶化的程度取决于修改的频率和代码的特定特征.
11.6自修改代码
对当前在处理器中高速缓存的代码段中的存储器位置的写入导致相关联的高速缓存行(或多个行)无效.此检查基于指令的物理地址.此外,P6系列和奔腾处理器检查对代码段的写入是否可以修改已经预取执行的指令.如果写入影响预取指令,则预取队列无效.后一种检查基于指令的线性地址.对于Pentium 4和Intel Xeon处理器,代码段中的指令的写入或窥探(其中目标指令已经被解码并驻留在跟踪高速缓存中)使整个跟踪高速缓存无效.后一种行为意味着在Pentium 4和Intel Xeon处理器上运行时,自我修改代码的程序可能会导致性能严重下降.
虽然有一个性能计数器来确定是否发生了不好的事情(C3 04 MACHINE_CLEARS.SMC:检测到自修改代码机器清除的数量)我想知道更多细节,特别是Haswell.我的印象是,只要我能够提前编写生成的代码,指令预取还没有到达那里,并且只要我不通过修改同一页面上的代码来触发SMC检测器(四分之一 - 页面?)作为当前正在执行的任何内容,那么我应该获得良好的性能.但所有的细节看起来都非常模糊:距离过近有多近?到目前为止还远吗?
试图将这些问题转化为具体问题:
Haswell预取器运行的当前指令前面的最大距离是多少?
Haswell"跟踪缓存"可能包含的当前指令后面的最大距离是多少?
Haswell上MACHINE_CLEARS.SMC事件的周期实际惩罚是多少?
如何在预测循环中运行生成/执行循环,同时防止预取程序吃掉自己的尾部?
我如何安排流程,以便每个生成的代码始终"第一次看到",而不是踩到已经缓存的指令?
我有四个2位位域存储在一个字节中.因此,每个位域可以表示0,1,2或3.例如,以下是前3个位域为零的4个可能值:
00 00 00 00 = 0 0 0 0
00 00 00 01 = 0 0 0 1
00 00 00 10 = 0 0 0 2
00 00 00 11 = 0 0 0 3
Run Code Online (Sandbox Code Playgroud)
我想要一种有效的方法来对四个位域进行求和.例如:
11 10 01 00 = 3 + 2 + 1 + 0 = 6
Run Code Online (Sandbox Code Playgroud)
现代Intel x64 CPU上的8位查找表需要4个周期才能从L1返回答案.似乎应该有一些方法来比这更快地计算答案.3个周期为6-12个简单位操作提供了空间.作为首发,简单的面具和换挡在Sandy Bridge上需要5个周期:
假设位字段是:d c b a,并且该掩码是:00 00 00 11
从艾拉帮助澄清:这假设a,b,c,和d是相同的,都被设置为初始byte.奇怪的是,我想我可以免费做到这一点.因为我每循环可以做2次加载,而不是加载byte一次,我可以加载它四次:a …
在R中有一个常见的函数调用模式,如下所示:
child = function(a, b, c) {
a * b - c
}
parent = function(a, b, c) {
result = child(a=a, b=b, c=c)
}
Run Code Online (Sandbox Code Playgroud)
这种重复的名称是有用的,因为如果要更改子项的参数的顺序,或者如果要将其他变量添加到列表中,它可以防止潜在的潜在错误:
childReordered = function(c, b, a) { # same args in different order
a * b - c
}
parent = function(a, b, c) {
result = childReordered(a, b, c) # probably an error
}
Run Code Online (Sandbox Code Playgroud)
但是当参数的名称变长时,这会变得很麻烦:
child = function(longVariableNameA, longVariableNameB, longVariableNameC) {
longVariableNameA * longVariableNameB - longVariableNameC
}
parent = function(longVariableNameA, longVariableNameB, longVariableNameC) {
child(longVariableNameA=longVariableNameA, …Run Code Online (Sandbox Code Playgroud) 我想强制预处理器为我做一些自动代码生成.我不需要太多:只是一个包含另一个for循环的简单for循环.[1]
我已经阅读了所有关于宏观扩展的内容,并且在蓝色涂料出现时不再傻笑.在美好的一天,我甚至可以解释为什么需要多层宏来生成带有标记粘贴的函数名称.我实际上已经让for-loop工作了.但是当涉及在循环中放置一个循环时,我会减少随机使用DEFER,EVAL和OBSTRUCT,并希望最好.
我不会被理智的召唤吓倒.我确实想用标准的C预处理器来做这件事.我保证无论结果如何,我,我的雇主和我的继承人都不会因技术弊端而起诉你.我保证如果没有适当的安全眼镜,我不会允许任何人维护代码,甚至不能查看代码.如果你愿意,假装我只是在考虑理论上的兴趣.或者我唯一的另一种选择是使用M4:因为如果CPP中的递归宏是变态的,那么M4肯定是全鸡.
我发现的最好的参考资料是一个有9年历史的Usenet线程:http: //comp.std.c.narkive.com/5WbJfCof/double-cpp-expansion
它开始偏离主题,在语气上有点小气和好斗,并且是我的头脑.但我认为我寻求的答案就在那里.
下一个最好的是CPP滥用标题的文档,称为Cloak:https: //github.com/pfultz2/Cloak/wiki/C-Preprocessor-tricks,-tips,-and-idioms
它需要一种不同的迭代方法,或许可以满足我的需求.但它也是一个很好的概述.
这里有一些简短的代码来显示我被卡住的地方.
repeat.h:
#define REPEAT(macro, times, start_n, next_func, next_arg, macro_args...) \
_REPEAT_ ## times(macro, start_n, next_func, next_arg, ## macro_args)
#define REPEAT_ADD_ONE(macro, times, start_n, macro_args... ) \
REPEAT(macro, times, start_n, _REPEAT_ADD_ONE, 0, ## macro_args)
#define _REPEAT_ADD_ONE(n, ignore...) _REPEAT_ADD_ONE_ ## n
#define _REPEAT_0(args...) /* empty */
#define _REPEAT_1(macro, n, func, i, args...) macro(n, ## args)
#define _REPEAT_2(m, n, f, i, a...) m(n, ## a); _REPEAT_1(m, f(n, i), f, i, ## …Run Code Online (Sandbox Code Playgroud) 我想知道如何为C64,C++或汇编语言编写x64处理器的高效跳转表.输入事先已知,但无法通过算法预测.假设我可以在输入流中看起来像我想要的那样遥远,有什么办法可以动态地告诉CPU下一个分支将要去哪个地址?
基本上,我想以编程方式更新分支目标缓冲区.但是,如果程序员事先知道下一个分支在哪里查看数据但处理器无法从过去的模式中确定这一点,那么我会解决任何允许我避免冲洗管道的问题.
意识到这是一个非常具体的问题,我可能无法正确地传达它,这里有一些替代措辞:
hbr在Cell处理器上是否有x64等效于分支的提示?
cmp像Itanium一样,它比条件分支更早地移动程序集是否有帮助?
间接跳转的预测目标是否基于寄存器值而不是最后使用的地址?
谢谢!
assembly x86-64 cpu-architecture jump-table branch-prediction
当我查看最近处理器的图表和概述时 [1],我从未看到提及 MMX 寄存器 MM0 - MM7。但从规格来看,它们似乎仍然存在。可以依赖它们存在于支持 SSE 的所有处理器中吗?除了更旧的 FPU 堆栈之外,它们是否与其他任何东西冲突?它们是否与一般 64 位的物理寄存器相同?
虽然 XMM 和 YMM 对于向量要好得多,但我偶尔想使用 MMX 寄存器来存储值,否则这些值会溢出到堆栈中。Speedwise 这看起来好一点,而且有时我想避免额外的商店和负载。