将1-100 int乘以-1或将所述int设置为零需要更多时间吗?

时间:2018-01-20 19:29:08

标签: c performance assembly

如果语言很重要,那就是C语言。如果它归结为汇编语言,它会使用两个补码将事物设置为负数。使用变量,您将值“0”存储在变量int中。我不完全确定会发生什么。

我得到:1.90s用户0.01s系统99%cpu 1.928总代码,我猜测大多数运行时都在加入计数器变量。

int i;
int n;

i = 0;
while (i < 999999999)
{
    n = 0;
    i++;
    n++;
}

我得到:4.56s用户0.02s系统99%cpu 4.613总计代码。

int i;
int n;

i = 0;
n = 5;
while (i < 999999999)
{
    n *= -1;
    i++;
    n++;
}
return (0);

我对组装并不是特别了解,但使用二进制补码操作比将一个组件设置为另一个组件花费更多时间似乎并不直观。什么是底层实现,使一个比另一个更快,以及表面下发生了什么?或者我的测试只是一个糟糕的测试,并没有准确地描述它在实践中的实际速度。

如果它看起来毫无意义,原因是因为我可以通过简单地将地图上的整数乘以-1来轻松实现“清单”,这意味着它已经被检查过(但我需要保留该值,所以当我做检查,我可以只是-1,无论我正在比较它)。但我想知道这是否太慢,我可以创建一个单独的布尔2D数组来检查值是否被检查,或者将我的数据结构更改为结构数组,以便它可以保存int 1/0。我想知道最好的实现是什么 - 做-1次操作本身十亿次已经总计大约5秒不计算我的程序的其余部分。但是,制作一个单独的10亿平方的int数组或创建一个十亿平方的结构似乎也不是最好的方法。

2 个答案:

答案 0 :(得分:2)

分配零非常便宜。

但是你的microbenchmark很少告诉你应该为你的大阵列做些什么。内存带宽/缓存未命中/缓存占用空间的考虑将在那里占主导地位,而您的microbench根本不会对此进行测试。

与具有单独的位图相比,使用一位整数值来表示已检查/未检查似乎是合理的。 (具有单独的0/1 32位整数数组将是完全愚蠢的,但位图值得考虑,特别是如果您想快速搜索下一个未选中或下一个选中的条目。不清楚你在做什么,所以我大多只是坚持解释微基准测试中观察到的性能。)

顺便说一句,这样的问题就是为什么SO评论如此“为什么你自己不对自己进行基准测试”被误导的一个很好的例子:因为你必须在很多细节上理解你正在测试的内容。有用的microbenchmark。

你显然是在调试模式下编译它,例如gcc使用默认的-O0,它会在每个C语句之后将所有内容溢出到内存中(因此即使使用调试器修改变量,程序仍然有效)。否则,循环会优化掉,因为你没有使用volatileasm语句来限制优化,而且你的循环很容易优化。

Benchmarking with -O0 does not reflect reality (of compiling normally), and is a total waste of time(除非你真的担心像游戏这样的调试版本的性能)。

那就是说,你的结果很容易解释:因为-O0分别和可预测地编译每个C语句。

  • n = 0;是只写,并打破了对旧值的依赖。

  • n *= -1;使用gcc与n = -n;编译相同(即使使用-O0)。它必须在写入新值之前从内存中读取旧值。

在语句中写入和读取C变量之间的存储/重新加载会花费大约5个周期的英特尔Haswell上的存储转发延迟(请参阅http://agner.org/optimize上的其他链接标签维基)。 (你没有说你测试过的CPU微体系结构,但我假设某种x86,因为它通常是“默认的”)。但在这种情况下,依赖性分析的工作方式仍然相同。

所以n*=-1版本有一个循环传承的依赖关系链,涉及nn++和否定。

n=0版本通过在不读取旧值的情况下执行商店来中断每次迭代的依赖性。该循环仅在i++循环计数器的6循环循环携带依赖性上产生瓶颈。 n=0; n++链的延迟无关紧要,因为每次循环迭代都会启动一个新链,因此可以同时运行多个链。 (存储转发提供了一种内存重命名,如寄存器重命名,但用于存储位置)。

这都是不切实际的废话:启用优化后,一元-的成本完全取决于周围的代码。您不仅可以将单独操作的成本加起来以获得总计,而不是流水线无序CPU的工作方式,而编译器优化本身也会使该模型变得虚假。

答案 1 :(得分:0)

关于代码本身

我使用GCC 7.2将您的代码片段编译成x86_64汇编输出而没有任何优化。我还缩短了每段代码而不更改汇编输出。结果如下。

代码1:

// C
int main() {
    int n;
    for (int i = 0; i < 999999999; i++) {
        n = 0;
        n++;
    }
}

// assembly
main:
  push rbp
  mov rbp, rsp
  mov DWORD PTR [rbp-4], 0
  jmp .L2
.L3:
  mov DWORD PTR [rbp-8], 0
  add DWORD PTR [rbp-8], 1
  add DWORD PTR [rbp-4], 1
.L2:
  cmp DWORD PTR [rbp-4], 999999998
  jle .L3
  mov eax, 0
  pop rbp
  ret

代码2:

// C
int main() {
    int n = 5;
    for (int i = 0; i < 999999999; i++) {
        n *= -1;
        n++;
    }
}

// assembly
main:
  push rbp
  mov rbp, rsp
  mov DWORD PTR [rbp-4], 5
  mov DWORD PTR [rbp-8], 0
  jmp .L2
.L3:
  neg DWORD PTR [rbp-4]
  add DWORD PTR [rbp-4], 1
  add DWORD PTR [rbp-8], 1
.L2:
  cmp DWORD PTR [rbp-8], 999999998
  jle .L3
  mov eax, 0
  pop rbp
  ret

循环中的C指令在程序集中位于两个标签(.L3:.L2:)之间。在这两种情况下,这是三条指令,其中只有第一条指令不同。在第一个代码中,它是mov,对应于n = 0;。但是,在第二个代码中,它是neg,对应于n *= -1;

根据this manual,这两条指令的执行速度不同,具体取决于CPU。在一个芯片上可以比另一个芯片快,而在另一个芯片上速度慢。 感谢aschepler在输入的评论中。

这意味着,所有其他指令都是相同的,您无法分辨哪些代码通常会更快。因此,试图比较他们的表现是毫无意义的。

关于你的意图

您询问这些短代码的性能的原因是错误的。你想要的是实现一个清单结构,你对如何构建它有两个相互矛盾的想法。一个使用特殊值-1,为地图中的变量添加特殊含义。另一个使用附加数据(外部布尔数组或每个变量的布尔值)来添加相同的含义而不改变现有变量的用途。

您必须做出的选择应该是一个设计决策,而不是由于不明确的性能问题。就个人而言,每当我在特殊值或具有精确含义的附加数据之间进行这种选择时,我倾向于选择后一种选择。这主要是因为我不喜欢处理特殊值,但这只是我的看法。

我的建议是寻求你能够更好地维护的解决方案,即你最熟悉的解决方案,不会损害未来的代码,并在重要时询问性能,或者甚至是否重要。