哪个代码块“更好”?
Posted
技术标签:
【中文标题】哪个代码块“更好”?【英文标题】:Which block of code is 'better'? 【发布时间】:2010-11-06 14:14:39 【问题描述】:为了培养良好的编程习惯,提高我的代码效率(阅读:“我哥和我在争论一些代码”),我向有经验的程序员提出这个问题:
对于那些懒得阅读代码的人来说,是否值得在 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 的话,“过早的优化是万恶之源。” 已经投了赞成票!我对大多数提供新思考的答案投了赞成票。它们绝对值得一读,尽管我个人认为 R. Pate 值得一读。 【参考方案1】:您可能会在方法开始/结束时在无意义且不必要的 [blocksToKill 保留]/[blocksToKill 释放] 上浪费更多时间,而不是执行几十个比较所花费的时间。无需保留该数组,因为您返回后将不再需要它,并且在此之前它永远不会被清理。
恕我直言,代码重复是导致错误的主要原因,应尽可能避免。
添加 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;
如果你完成了你的项目并且你的程序运行得不够快(两个大的 if),那么你可以分析它并找到热点,然后你可以确定你花在考虑那个分支上的几微秒是否值得考虑— 当然,现在根本不值得考虑,这意味着唯一的考虑是哪个更具可读性/可维护性。
【讨论】:
我完全同意你的观点,彼得。事实上,人们对每次评估numOfBlocks > 3
的质疑不仅是在猜测编译器(它可以通过提升未修改变量的测试来优化),而且通常会忽略调用 -objectAtIndex: 每次循环要慢得多这一点而不是 for-in 循环。在这段代码中,你用一块石头杀死了两只鸟,这是一个简洁而快速的解决方案。这个答案你应该得到更多的投票。【参考方案2】:
可读性(因此可维护性)可以而且应该以性能为名牺牲,但是当且仅当确定性能是一个问题时。
第二个块更具可读性,除非/直到速度成为问题,否则它会更好(在我看来)。在测试您的应用程序期间,如果您发现此循环导致无法接受的性能,那么请尽一切努力使其更快,即使它变得更难维护。但在必要之前不要这样做。
【讨论】:
【参考方案3】:如果不明确您对“更好”的要求,就无法回答这个问题。是运行时效率吗?编译大小?代码可读性?代码可维护性?代码可移植性?代码可重用性?算法证明?开发效率? (请留下我错过的任何流行测量值。)
有时绝对的运行时效率才是最重要的,但并不像人们通常想象的那样频繁,正如您在问题中所表示的那样——但这至少很容易测试!通常是所有这些问题的混合体,最终您必须做出主观判断。
这里的每个答案都应用了这些方面的个人组合,人们经常陷入激烈的圣战,因为每个人都是正确的——在正确的情况下。这些方法最终都是错误的。唯一正确的方法是定义对您而言重要的事情,然后对其进行衡量。
【讨论】:
代码可读性和代码可维护性通常是最重要的,因为它会导致更少的错误和更稳定的代码。 (并让编译器为您优化)... 出于某种原因,我没有具体说明“更好”的含义。我已经知道这两个代码块的优缺点。我想知道这里的其他程序员眼中的“更好”是什么。但是,您会因为获得最深刻的反应而获得绿色检查。 Johan:我会说这些会导致更轻松的开发,这对开发人员来说自然很重要。当然,其他事情也会导致更容易的开发,更容易的开发本身会导致更少的错误、更快的周转(包括错误修复)和更好的代码。但是,您的评论中的“通常”仍然很重要,并且声明“代码可读性和可维护性是重要要求”是微不足道的,将其与其他要求进行排名(例如“代码始终有效”,这通常是最重要但未说明的),然后衡量您如何满足您的要求。【参考方案4】:“if”测试的开销是少量 CPU 指令;不到一微秒。除非您认为循环将运行数十万次以响应用户输入,否则这只是在噪音中丢失。所以我会选择第二种解决方案,因为代码更小更容易理解。
不过,无论哪种情况,我都会将循环更改为
for (temp in blocksToKill) ...
这比手动获取数组的每个元素更清晰且速度更快。
【讨论】:
这看起来很有趣,(temp in blocksToKill) 是否与 (NSUInteger i = 0; i 基本上是的,但是根据集合类,它可能会做一些更复杂的事情。例如,它可以通过发送一条消息获取接下来的 100 个对象,然后遍历这些指针,依次获取一批对象。【参考方案5】:我的投票强烈支持第二个区块。
第二个块明确了逻辑上的区别,并且共享相同的循环结构。它更具可读性和可维护性。
第一个块是过早优化的一个例子。
至于使用 bool 来“保存”所有这些 LTE 比较 - 在这种情况下,我认为它不会有帮助,机器语言可能需要完全相同数量和大小的指令。
【讨论】:
【参考方案6】:我会选择第二个选项。如果循环中的所有逻辑都完全不同,那么创建 2 个 for 循环是有意义的,但情况是有些逻辑是相同的,有些是基于条件的附加逻辑。所以第二种选择更干净。
第一个选项会更快,但速度稍快,我只会在发现那里存在瓶颈时才使用它。
【讨论】:
【参考方案7】:在所有其他条件相同的情况下,拥有两个单独的循环通常会更快,因为您只进行一次测试而不是循环的每次迭代。由于管道停顿和分支错误预测,每次迭代循环内的分支通常会显着减慢您的速度;但是,由于分支总是以相同的方式进行,因此 CPU 几乎肯定会为每次迭代正确预测分支,除了前几次,假设您使用的 CPU 具有分支预测功能(我不确定是否使用了 ARM 芯片在 iPhone 中有一个分支预测器单元)。
但是,要考虑的另一件事是代码大小:两个循环方法会生成更多代码,尤其是在循环体的其余部分很大的情况下。这不仅会增加程序目标代码的大小,还会损害指令缓存性能——您将获得更多的缓存未命中。
考虑到所有因素,除非代码是应用程序中的重大瓶颈,否则我会选择循环内部的分支,因为它会导致代码更清晰,并且不会违反don't repeat yourself principle。如果您对其中一个循环进行了更改而忘记更改双循环版本中的另一个循环,那么您将陷入痛苦的世界。
【讨论】:
我更喜欢这个答案,因为这意味着我是对的,这是错的吗?【参考方案8】:第一个代码块更简洁、更高效,因为在整个迭代过程中检查 numOfBlocks > 3 是真还是假。
第二个代码块避免了代码重复,因此可能带来的风险较小。但是,它在概念上更复杂。
第二块可以通过添加来改进
bool increaseChargeTotal = (numOfBlocks > 3)
在循环之前,然后使用这个布尔变量代替循环内的实际检查,强调在迭代期间它不会改变的事实。
就个人而言,在这种情况下,我会投票给第一个选项(重复循环),因为循环体很小,这清楚地表明条件在循环之外;此外,它更高效,可能符合“使常见情况快速”的模式。
【讨论】:
看来我哥哥赢了。感谢您的回复速度和质量!这个网站太棒了。 如果他要进行这种优化(过早:) 他不应该也将调用移出两个循环吗?以上是关于哪个代码块“更好”?的主要内容,如果未能解决你的问题,请参考以下文章