哪个代码块更好?

3
为了推广良好的编程习惯并提高代码效率(意思是“我和我兄弟正在争论一些代码”),我向经验丰富的程序员提出以下问题:
哪一个代码块更“好”?对于那些不愿意阅读代码的人,值得在for循环内部放置一个条件以减少冗余代码量,还是将其放在外面并制作2个for循环?两个代码片段都可以工作,问题在于效率与可读性。
    - (NSInteger)eliminateGroup {
            NSMutableArray *blocksToKill = [[NSMutableArray arrayWithCapacity:rowCapacity*rowCapacity] retain];
            NSInteger numOfBlocks = (NSInteger)[self countChargeOfGroup:blocksToKill];
            Block *temp;
            NSInteger chargeTotal = 0;

//Start paying attention here

            if (numOfBlocks > 3) 
                for (NSUInteger i = 0; i < [blocksToKill count]; i++) {
                    temp = (Block *)[blocksToKill objectAtIndex:i];
                    chargeTotal += temp.charge;
                    [temp eliminate];
                    temp.beenCounted = NO;
                }
            }
            else {
                for (NSUInteger i = 0; i < [blocksToKill count]; i++) {
                    temp = (Block *)[blocksToKill objectAtIndex:i];
                    temp.beenCounted = NO;
                }
            }   
            [blocksToKill release];
            return chargeTotal;
        }

或者...
        - (NSInteger)eliminateGroup {
            NSMutableArray *blocksToKill = [[NSMutableArray arrayWithCapacity:rowCapacity*rowCapacity] retain];
            NSInteger numOfBlocks = (NSInteger)[self countChargeOfGroup:blocksToKill];
            Block *temp;
            NSInteger chargeTotal = 0;

//Start paying attention here

            for (NSUInteger i = 0; i < [blocksToKill count]; i++) {
                temp = (Block *)[blocksToKill objectAtIndex:i];
                if (numOfBlocks > 3) {
                    chargeTotal += temp.charge;
                    [temp eliminate];
                }
                temp.beenCounted = NO;
            }
            [blocksToKill release];
            return chargeTotal;
        }

请记住,这是针对游戏的。每当用户双击屏幕时,该方法被调用,for循环通常运行1到15次迭代,最多64次。我知道这并不重要,主要是为了帮助我了解条件语句的成本有多高。(即:我只想知道我是否正确。)


首先,这两个示例都可以从for-in循环中受益 - 请参见Peter Lewis的示例,并在那里点赞。我建议使用他对第二种形式的改编以获得更清晰的代码。引用Donald Knuth的话,“过早优化是万恶之源”。 - Quinn Taylor
已经点赞了!我已经给大多数提供了新思路的答案点赞了。它们绝对值得一读,尽管我个人认为 R. Pate 值得获得奖励。 - Tozar
8个回答

9
第一个代码块更加干净高效,因为检查 numOfBlocks > 3 在整个迭代过程中要么为真要么为假。
第二个代码块避免了代码重复,因此可能存在较小的风险。然而,它在概念上更加复杂。
通过添加,第二个代码块可以改进。
bool increaseChargeTotal = (numOfBlocks > 3)

在循环之前使用布尔变量,而不是在循环内部实际检查,并强调在迭代期间它不会改变。就我个人而言,在这种情况下,我会选择第一种选项(重复循环),因为循环体很小,很清楚地表明条件是循环外部的;而且,它更有效率,可能符合“使常见情况快速”的模式。

看起来我哥哥赢了。感谢您快速和高质量的回复!这个网站太棒了。 - Tozar
如果他要进行这种(过早的 :))优化,他是否也应该将对count的调用移出两个循环? - jmucchiello

8
其他条件相同的情况下,拥有两个单独的循环通常会更快,因为你只需要进行一次测试而不是每次循环迭代都需要测试。循环内部的分支每次迭代都会经常因为流水线停顿和分支预测错误而减慢速度;然而,由于分支总是以相同的方式进行,CPU几乎肯定会对每次迭代正确地预测分支,除了前几次迭代之外,假设你使用带有分支预测功能的CPU(我不确定iPhone中使用的ARM芯片是否具有分支预测器单元)。
然而,另一个要考虑的因素是代码大小:两个循环方法生成了大量的代码,特别是如果循环体的其余部分很大。这不仅增加了程序对象代码的大小,而且还会影响指令缓存性能--你会得到更多的缓存未命中。
综上所述,除非代码在应用程序中是一个重要的瓶颈,否则我会选择循环内部的分支,因为它导致代码更清晰,而且不违反不要重复自己的原则。如果你对两个循环版本中的一个进行更改并忘记更改另一个循环,则会遇到一系列问题。

我喜欢这个答案,因为它意味着我是正确的,这有错吗? - Tozar

8
没有定义“更好”的需求,无法回答这个问题。是运行时效率?编译后大小?代码可读性?代码可维护性?代码可移植性?代码可重用性?算法可证明性?开发人员效率?(如果我漏掉了任何流行的衡量标准,请在评论中指出。)
有时候绝对的运行时效率是最重要的,但并不像人们通常想象的那样经常出现,正如你在问题中提到的一样-但至少这很容易测试!通常情况下,这些问题都是综合考虑的,最终你必须做出主观判断。
每个答案都应用了个人的这些方面的混合,而且人们经常陷入激烈的圣战之中,因为每个人都是正确的-在正确的情况下。这些方法最终都是错误的。唯一正确的方法是 定义对你来说重要的事情,然后根据它进行测量

1
代码的可读性和可维护性通常是最重要的,因为它可以减少错误并使代码更加稳定。(让编译器为您进行优化)... - Johan
我之所以没有明确说明“更好”的含义,是有原因的。我已经知道了这两个代码块的优缺点。我想知道在其他程序员眼中哪一个更“好”。但你因为给出了最深刻的回答而获得了绿色勾选标记。 - Tozar
1
Johan:我会说这些可以导致更容易的开发,这对开发人员来说自然很重要。当然,其他事情也会导致更容易的开发,而更容易的开发本身会导致更少的错误、更快的反应时间(包括修复错误),以及更好的代码。但是,你评论中的“通常”仍然很重要,而且将“代码可读性和可维护性是重要的要求”与其他要求(如“代码始终正常运行”,这通常是最重要但未被说明的)进行比较,并衡量您如何满足您的要求是微不足道的。 - Roger Pate

5

在这个方法的开头或结尾使用[blockToKill retain] / [blockToKill release] 是毫无意义和不必要的,你浪费的时间可能比执行几十次比较还多。由于你在返回后不需要该数组,并且在那之前它永远不会被清除,因此没有必要保留该数组。

在我看来,代码重复是Bug的主要原因之一,应尽可能避免。

将Jens的建议添加到使用快速枚举和Antti的建议中使用一个明确命名的布尔变量,你可以得到以下代码:

    - (NSInteger)eliminateGroup {
        NSMutableArray *blocksToKill = [NSMutableArray arrayWithCapacity:rowCapacity*rowCapacity];
        NSInteger numOfBlocks = (NSInteger)[self countChargeOfGroup:blocksToKill];
        NSInteger chargeTotal = 0;

        BOOL calculateAndEliminateBlocks = (numOfBlocks > 3);
        for (Block* block in blocksToKill) {
            if (calculateAndEliminateBlocks) {
                chargeTotal += block.charge;
                [block eliminate];
            }
            block.beenCounted = NO;
        }
        return chargeTotal;
    }

如果你完成了项目,但程序运行速度不够快(有两个很大的假设),那么你可以对其进行性能分析,找出热点,然后确定你花费在思考该分支的少数微秒是否值得——当然现在完全没有必要考虑这个,这意味着唯一需要考虑的是哪个更易读/易于维护。


我完全同意你的观点,Peter。事实上,那些对每次评估numOfBlocks > 3争论不休的人不仅是在怀疑编译器(它可以通过提升未修改变量的测试来进行优化),而且通常忽视了每次循环调用-objectAtIndex:比使用for-in循环要慢得多的事实。这段代码一举两得,既简洁又快速。你对这个答案应该获得更多的赞同票。 - Quinn Taylor

5

我会选择第二个选项。如果循环中的所有逻辑完全不同,那么创建两个for循环是有意义的,但情况是某些逻辑相同,而某些逻辑基于条件附加。因此,第二个选项更加简洁。

第一个选项会更快,但仅略微如此,只有在发现瓶颈时才会使用它。


4

我强烈支持第二个块。

第二个块明确说明了逻辑的不同之处,并且具有相同的循环结构。它更易读和易于维护。

第一个块是提前优化的例子。

至于使用布尔值来“保存”所有LTE比较--在这种情况下,我认为它并没有帮助,机器语言可能需要完全相同数量和大小的指令。


3
“if”测试的开销只有几个CPU指令,远少于一微秒。除非你认为循环会因用户输入而运行数十万次,否则这只是噪音中丢失的东西。所以我会选择第二个解决方案,因为代码既更小,也更易于理解。
但无论哪种情况,我都会更改循环为
for (temp in blocksToKill) { ... }
这样做更容易阅读,而且比手动获取数组的每个元素快得多。

这看起来很有趣,(temp in blocksToKill)和for (NSUInteger i = 0; i < [blocksToKill count]; i++)做的是同样的事情吗? - Tozar
基本上是的,不过具体操作可能还要取决于集合类的类型。例如,它可以通过一次消息发送获取接下来的100个对象,然后迭代这些指针,依次获取对象的批次。 - Ashley Clark

0

为了性能,可牺牲易读性(因此也就是可维护性),但前提是已经确定性能是一个问题。

第二个代码块更易读,在速度不是问题的情况下,它更好(在我看来)。在测试应用程序时,如果发现这个循环导致性能不佳,那么请尽一切可能使其更快,即使这样会更难以维护。但在必要之前不要这样做。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接