我想知道什么是我写代码的最快方法。我有一个循环,在一些int上执行添加。循环将执行很多次,所以我想进行比较以检查是否有任何操作数为零,所以不应该认为它们被添加,如下所示:
if (work1 == 0)
{
if (work2 == 0)
tempAnswer = toCarry;
else
tempAnswer = work2 + toCarry;
}
else if (work2 == 0)
tempAnswer = work1 + toCarry;
else
tempAnswer = work1 + work2 + toCarry;
我认为顶部的嵌套IF已经是一个优化,因为它比用&&'s编写一系列比较要快,因为我会不止一次地检查(work1 == 0)
。 / p>
可悲的是,我无法说出work1和work2的频率为零,所以假设它可能是IF语句的每个可能结果的均衡分布。
那么,鉴于此,上述代码是否比仅仅编写tempAnswer = work1 + work2 + toCarry
更快或者所有比较是否会导致很多拖累?
由于
答案 0 :(得分:26)
这是无稽之谈。
在更现代的架构中,瓶颈是从内存中访问值,因此该方案仍无法满足需要。
另外,从逻辑上考虑一下 - 为什么单独将零视为特殊情况下的一个值?为什么不检查一个,并使用tempAnswer++
?当你考虑所有可能性时,你会发现这是一个毫无意义的练习。
答案 1 :(得分:17)
答案一如既往地描述您的代码。写两种方式,计时,看哪种方式更快。</ p>
那就是说,我的钱会直接加成比一堆比较更快。每次比较都意味着一个潜在的分支,分支可能会对处理器中的流水线造成严重破坏。
答案 2 :(得分:2)
分支很可能比添加速度慢,所以这可能会适得其反。无论如何,阅读起来要困难得多。在你有充分的证据表明你需要它之前,你真的不应该尝试优化到这个水平。对代码的负面影响通常不值得。
答案 3 :(得分:2)
不,它并不快。分支错误预测比添加更加痛苦。
答案 4 :(得分:1)
在执行添加之前有条件检查的唯一情况是节省时间,如果可以避免“昂贵”的写操作。例如,像:
if (var1 != 0) someobject.property1 += var1;如果写入propert1的速度很慢,
可以节省时间,特别是如果该属性尚未优化写出已存在的值。在极少数情况下,人们可能会受益于:
if (var1 != 0) volatilevar2 += var1;
如果多个处理器都经常重新读取volatilevar2,则var1通常为零。令人怀疑的是,那种有益的比较会“自然地”发生,尽管可能会有人作风。一个稍微有点人为的版本:
if (var1 != 0) Threading.Interlocked.Add(volatilevar2, var1);
在一些自然发生的场景中可能是有益的。
当然,如果添加的目的地是不与其他处理器共享的本地临时变量,则节省时间的可能性基本上为零。
答案 5 :(得分:1)
除了比较通常和添加一样快(因此平均来说你有更多的操作),以及在许多架构分支的情况下,如果CPU无法猜测哪个分支是昂贵的这一事实它的方式,也有代码的位置。
现代处理器尽可能地保留在处理器中的缓存中,或者可能在主板上。点击主存储器相对较慢,并且在存储器页面中读取相对非常慢。从快速和小到慢和大的层次结构。性能的一个重要方面是尝试保持在该层次结构的“快速和小”方面。
您的代码将处于循环中。如果该循环适合一个或两个缓存行,那么你的状态很好,因为CPU可以用极短的时间执行循环来获取指令,而不会从缓存中踢出其他内存。
因此,在进行微优化时,您应该尝试让内部循环包含小代码,这通常意味着简单和简短。在你的情况下,当你没有比较和两个加法时,你有三个比较和几个加法。此代码比更简单的tempAnswer = work1 + work2 + toCarry;
更容易导致缓存未命中。
答案 6 :(得分:1)
最快是一个相对术语。这个平台是什么?它有缓存吗?如果它具有高速缓存,则可能在可以在单个时钟周期内执行添加的平台上,因此不需要优化添加。下一个问题是比较是减去减法和添加通过相同的alu并采取相同的时间添加,所以对于大多数平台旧的和新的交易比较(减法)添加不会保存你的任何东西,你最终看着分支成本,管道冲洗等。即使使用ARM平台,您仍然可以烧掉一些或几个。对于这样的优化,您必须做的第一件事是查看编译器输出,编译器选择哪些指令? (假设这是编译器,每个编译此代码的编译器都使用相同的编译器选项等)。例如,在加/减占用时钟或大量时钟的芯片上,xor或和/或操作可能需要更少的时钟。您可以使用按位操作在某些处理器上进行零比较,从而节省时钟。编译器是否已将其解决并使用更快的操作?
作为对您的问题的一般目的回答,基于那里的处理器以及您正在使用或未使用的处理器的可能性。单行:
tempAnswer = work1 + work2 + toCarry;
是最优化的代码。对于大多数处理器或我猜你可能正在使用的处理器,编译器会将其转换为两个或三个指令。
您更大的担忧不是添加或比较或分支或分支预测,您最担心的是这些变量保存在寄存器中。如果他们都必须来回堆栈/ ram,这将减慢你的循环,即使使用缓存。循环中的其他代码将确定这一点,并且您可以在代码中执行一些操作以最大限度地减少寄存器使用,从而允许这些代码基于寄存器。再次,反汇编代码以查看编译器正在做什么。
答案 7 :(得分:1)
我同意其他评论的一般主张 - “优化”实际上是一种“悲观化”,使得代码更难以编写,阅读和维护。
此外,'优化'代码比简单代码更大。
$ cat yy.c
int optimizable(int work1, int work2, int toCarry)
{
int tempAnswer;
if (work1 == 0)
{
if (work2 == 0)
tempAnswer = toCarry;
else
tempAnswer = work2 + toCarry;
}
else if (work2 == 0)
tempAnswer = work1 + toCarry;
else
tempAnswer = work1 + work2 + toCarry;
return tempAnswer;
}
$ cat xx.c
int optimizable(int work1, int work2, int toCarry)
{
int tempAnswer;
tempAnswer = work1 + work2 + toCarry;
return tempAnswer;
}
$
$ gcc --version
gcc (GCC) 4.1.2 20080704 (Red Hat 4.1.2-44)
Copyright (C) 2006 Free Software Foundation, Inc.
This is free software; see the source for copying conditions. There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
$ gcc -c yy.c xx.c
$ size xx.o yy.o
text data bss dec hex filename
86 0 0 86 56 xx.o
134 0 0 134 86 yy.o
$ gcc -O -c yy.c xx.c
$ size xx.o yy.o
text data bss dec hex filename
54 0 0 54 36 xx.o
71 0 0 71 47 yy.o
$ gcc -O1 -c yy.c xx.c
$ size xx.o yy.o
text data bss dec hex filename
54 0 0 54 36 xx.o
71 0 0 71 47 yy.o
$ gcc -O2 -c yy.c xx.c
$ size xx.o yy.o
text data bss dec hex filename
54 0 0 54 36 xx.o
70 0 0 70 46 yy.o
$ gcc -O3 -c yy.c xx.c
$ size xx.o yy.o
text data bss dec hex filename
54 0 0 54 36 xx.o
70 0 0 70 46 yy.o
$ gcc -O4 -c yy.c xx.c
$ size xx.o yy.o
text data bss dec hex filename
54 0 0 54 36 xx.o
70 0 0 70 46 yy.o
$
代码是针对AMD x86-64上的64位RedHat Linux编译的。
这两个功能携带相同的基础设施行李(3个参数,1个本地,1个返回)。最好的情况是,优化函数比未优化函数长16个字节。将额外的代码读入内存会降低性能,执行该代码所需的额外时间是另一个。
答案 8 :(得分:0)
这是经典的警告:“避免早期优化”。
这个功能真的很重要吗?它被调用了很多次,你必须对它进行优化吗?
现在,让我们看看@ Jonathan的回答并考虑“技术债务”,即可维护性。在您的特定环境中思考:在一两年内,有人会查看您的代码并发现它更难以理解,或者更糟糕的是,他/她会误解它!
最重要的是,比较xx.c和yy.c:哪一段代码有更高的错误机会?
祝你好运!