让我们首先解释每条指令的作用。REP OPD 的工作原理如下:
for (; ecx > 0; ecx--) OPD
该指令将重复操作数,同时递减ECX直到达到0。请注意,在您的代码中,ECX设置为 13(地址 0040106A)。
另一方面,STOS OPD 将AL或AX或EAX的值存储在给定的内存操作数中。寄存器大小由内存位置大小定义,因此是代码中的DWORD。
总的来说,这两条指令结合起来创建了一个将数据存储在内存中的循环。现在把东西放在C形式中,如果我们想将一个字节数组初始化为0(例如 memset),我们可以这样做:
unsigned char t[MAX_CHAR];
for (int i = 0; i < MAX_CHAR; i++)
t[i] = 0;
此 C 代码可以转换为多个等价的汇编代码。它主要取决于编译器和指定的优化级别。基于 REP STOS 的一种变体可能是:
mov ecx, MAX_CHAR //Initialize ecx to the number of iterations desired
xor eax, eax //Initialize eax to 0
rep stos [t + ecx] //Store the value of eax in t[i] where i = ecx
另一个等效的汇编变体可能是:
mov ecx, MAX_CHAR //Initialize ecx to the number of iterations
xor eax, eax //Initialize eax to 0
loop0: //Define loop label
mov [t + ecx], eax //Copy eax into t[i] where i = ecx
dec ecx //Decrement ecx ==> ecx = ecx - 1
jnz loop0 //Jump only if previous result (ecx - 1) isn't 0
这两个汇编代码完全相似。唯一的区别是,在周期方面,一个会比另一个花费更多。换句话说,一个比另一个快。如何定义哪个是最好的?如果您查看第 162 页的Agner Fog 指令表,您会注意到在最坏的情况下,REP STOS延迟为n(n 是迭代次数)。如果您查看指令表,您会发现XOR REG, SAME将花费我们 0.25 个周期,而MOV R, IMM将花费我们 0.5 个周期。总的来说,第一个汇编代码的性能可以评估为:latency = n + 0.75周期(对于 1000 次迭代 ~ 1000.75 个周期)。如果我们查看第二个汇编代码,我们会得到以下内容:
mov ecx, MAX_CHAR //0.50 cycles
xor eax, eax //0.25 cycles
loop0:
mov [t + ecx], eax //1 cycle
dec ecx //2 cycles
jnz loop0 //1 cycle
在这种情况下,我们得到延迟 = 4 * n + 0.75。
现在,您可能认为第一个代码比第二个代码快,因为n < 4n。不要忘记英特尔架构是流水线的,还有一些其他的奇怪之处。不过,我可以向您保证,第一个代码与第二个代码相比非常慢。为什么?代表斯托斯是微编码的(Fused µOps 列)。意思是,它不是真正的硬件指令。它本质上是通过将多个电路(此处为两个)组合而成的,这种方式在过去可以 - 使用 Pentium III 和 IV - 节省您的时间。这种方法的问题在于它在管道中并不适用。第二个汇编代码会快得多,因为所有指令都是硬编码的。这意味着,每个电路都有一个特殊的电路,不需要组合其他电路来执行所需的任务。因此,这些指令可以很容易地流水线化,因此计算出的4n总延迟可以远低于n(如果一切都被正确缓存)。对于所有三个指令,整个循环迭代将下降到大约 0.75 个周期。加上分支预测器正确命中时的奖励。
我必须提醒您,这些指令是在CPU难以有效运行循环构造的时候出现的。特别是对于字符串处理,因为分支太多,缓存太小。随着高效的分支预测器和更大/更复杂的缓存系统的出现,这些指令不知何故变得过时了。很少有编译器会根据REP STOS或REP LODS生成代码,除非您在一些非常旧的CPU上运行代码或使用非常旧的编译器(例如Turbo C)。
我希望我的帖子有助于阐明这些说明以及如何使用它们。如果您需要更多详细信息或更多解释,请告诉我。