【问题标题】:Should I minimize the number of "if" statements or "for" loops?我应该尽量减少“if”语句或“for”循环的数量吗?
【发布时间】:2013-10-14 09:41:41
【问题描述】:

我有一个对象列表,需要对每个元素应用多个有条件的操作。采用“if-for”方法还是“for-if”方法更有效。换句话说,我应该尽量减少if 语句的数量还是for 循环的数量?有这个标准吗?

确定这一点的可靠方法是什么?

“If-For” 最小化 if 语句

public void ifForMethod() {
    if (conditionA) {
        for (Object o : listOfObjects) {
            doA(o);
        }
    }

    if (conditionB) {
        for (Object o : listOfObjects) {
            doB(o);
        }
    }
}

“For-If” 最小化for循环的方法

public void forIfMethod() {
    for (Object o : listOfObjects) {
        if (conditionA) {
            doA(o);
        }
        if (conditionB) {
            doB(o);
        }

    }
}

假设

  • 条件是简单的布尔值,在迭代时不会改变。
  • 一个或多个条件为真。 (有2个以上的条件)
  • 每个条件都独立于其他条件。
  • 内部方法之间完全不冲突或交互。它们的执行顺序无关紧要。

【问题讨论】:

  • 第二个更有意义。它表明您将始终循环遍历元素,然后根据条件更改元素。它也可能与多线程有关。如果条件A 或条件B 在循环中发生变化,“For-If”将提供与“If-For”不同的结果。这取决于你想要完成什么。如果条件A 可以与条件B 同时为真,那么“For-If”的效率更高。
  • 什么对你来说更具可读性?两者都是 O(n) ,只有在最坏的情况下才会改变。
  • 这取决于doAdoB 做什么。
  • 标准是爱因斯坦所说的:“一切都应该尽可能简单,但不能简单。”
  • @Obicere 条件每次都会以相同的方式评估。实际上有几个条件,平均 75% 是正确的。

标签: java performance if-statement for-loop


【解决方案1】:

我认为这取决于更多的事情,例如:如果找到条件A,循环会中断吗?条件A和条件B可以共存吗?我可以和他们一起使用 if-else 吗?

只是寻找你所呈现的,我认为第二种方法更好。您只循环一次并在同一个循环中检查两次。在我看来,它也更具可读性。

【讨论】:

  • 循环不会中断,是的,所有条件都可以。
【解决方案2】:

首先,让我们看一下您目前所展示的方法的复杂性:

  • ifForMethod 执行 k 检查,其中 m 返回 true。对于这些 m 中的每一个,都有一个迭代 n 个对象。那么,复杂度是k+nm
  • forIfMethod 迭代 n 个对象并在每次迭代中执行 k 个比较。那么,复杂度是k+n(k-1)=nk

在这两种情况下,所有 k 条件都必须至少评估一次,因此这里的区别实际上在于 nmn(k-1 ) 加法。渐近地,m 只是 k 的一小部分(你说 m 大约是 .75k),所以这些都是 O(nk),但是 k+nm ,所以 ifForMethod 可能比 forIfMethod 快​​。实际运行时间的差异将取决于诸如迭代数组所需的实际时间以及 k 的大小等因素。您将开始处理诸如内存局部性之类的问题(对于您的对象以及您的代码)。

不过,您可能会觉得这是一种有趣的方法。理想情况下,您只想遍历对象列表一次,而不必多次检查布尔条件。您可以抽象出您正在执行的操作,这样您就可以将它们组合成一个单独的操作(并且您只会合并那些与真实条件相对应的操作),然后执行该复合操作列表中的每个元素。这是一些执行此操作的代码。

想法是存在Action,您可以构造一个执行doA 的Action 和一个执行doB 的Action。根据条件,您可以创建一个复合操作,如果doA 条件为真,则包含doA 操作,如果doB 条件为真,则包含doB 操作。然后您遍历对象,并调用对每个对象执行复合操作。渐近地说,这是一种 k+nm 方法,因此理论上它的性能很好,但同样,这里的实际性能将取决于一些棘手的常数和内存局部性问题。

import java.util.ArrayList;
import java.util.List;

public class CompoundActionExample {

    /**
     * An action is used to do something to an argument.
     */
    interface Action {
        void act( Object argument );
    }

    /**
     * A compound action is an action that acts on an argument
     * by passing the argument to some other actions.
     */
    static class CompoundAction implements Action {
        /**
         * The list of actions that the compound action will perform.  Additional
         * actions can be added using {@link #add(Action)}, and this list is only
         * accessed through the {@link #act(Object)} method.
         */
        private final List<CompoundActionExample.Action> actions;

        /**
         * Create a compound action with the specified list of actions.
         */
        CompoundAction( final List<CompoundActionExample.Action> actions ) {
            this.actions = actions;
        }

        /**
         * Create a compound action with a fresh list of actions.
         */
        CompoundAction() { 
            this( new ArrayList<CompoundActionExample.Action>() );
        }

        /**
         * Add an action to the compound action.
         */
        public void add( CompoundActionExample.Action action ) {
            actions.add( action );
        }

        /**
         * Act on an argument by passing the argument to each of the 
         * compound action's actions.
         */
        public void act( final Object argument) {
            for ( CompoundActionExample.Action action : actions ) {
                action.act( argument );
            }
        }
    }

    public static void main(String[] args) {
        // Some conditions and a list of objects
        final boolean conditionA = true;
        final boolean conditionB = false;
        final Object[] listOfObjects = { "object1", "object2", "object3" };

        // A compound action that encapsulates all the things you want to do
        final CompoundAction compoundAction = new CompoundAction();

        // If conditionA is true, add an action to the compound action that 
        // will perform doA.  conditionA is evaluated exactly once.
        if ( conditionA ) {
            compoundAction.add( new Action() {
                public void act( final Object argument) {
                    System.out.println( "doA("+argument+")" ); // doA( argument );
                }
            });
        }

        // If conditionB is true, add an action to the compound action that
        // will perform doB. conditionB is evaluted exactly once.
        if ( conditionB )  {
            compoundAction.add( new Action() {
                public void act(Object argument) {
                    System.out.println( "doB("+argument+")" ); // doB( argument );
                }
            });
        }

        // For each object, apply the compound action
        for ( final Object o : listOfObjects ) {
            compoundAction.act( o );
        }
    }
}

【讨论】:

  • 这是一个非常有趣的设计理念。该对象的创建与一遍又一遍地执行简单的布尔检查相比如何?
  • @MikeRylander 我刚刚评论了这个问题,但我认为(而且我还没有测试过),随着 k (条件的数量)变得越来越大,这种方法将优于它,因为迭代复合动作中的固定动作列表将比重新检查额外条件更少工作。对于小的 k,它可能不值得(无论如何,在速度方面;在更复杂的架构中,这也有一些可扩展性的好处),但我希望你会看到改进,因为 k 变大。
  • @MikeRylander 当然,如果布尔条件的计算成本很高(即,如果它们不是简单的布尔变量),那么不必在每个条件上一次又一次地计算它们会有很大的好处迭代。当我把答案放在一起时,还没有弄清楚它们是固定的还是动态的。
  • @MikeRylander 对象 creation 应该不是什么大问题。这只是 m 个正在创建的对象。如果动作定义明确且固定(在本例中就是这样),您可以提前定义它们并将它们放在某个地方,以便在必要时添加到复合动作中。因此,对象创建不会显着减慢速度;问题是额外的方法调度是否会(因为现在有一个间接级别;你不只是调用doA,你调用一个对象的act方法,然后调用doA)。
  • 不得不说这是对问题的透彻分析。您对相对复杂性提供了很好的解释,并指出复杂性并不是性能的保证。为了更进一步,您提供了一种出色的替代方法,可以解决其他方法的缺点。
【解决方案3】:

使用您所谓的“If-For”方式而不是“For-If”是一种称为loop unswitching 的优化(也许是更通用的版本)。它是否真的是一个好主意取决于几个因素,例如(但不限于)

  • 是否允许这种转换(即条件没有副作用,doAdoB 可能会重新排序)
  • 您正在优化什么(例如速度、可读性或 w/e),但在这种情况下并没有真正的影响
  • 数组是否适合缓存(迭代两次可能会使缓存未命中数翻倍)
  • (JIT) 编译器对它的作用究竟是什么,例如条件是否实际编译为分支,或者编译器是否会为您解除循环切换
  • 处理器微架构(一些 µarch 比其他人更不喜欢循环内的分支,即使这些分支是高度可预测的)

【讨论】:

    【解决方案4】:

    没有理由对列表进行 2 次通过。

    假设:谓词是简单的布尔值,如果必须对其进行评估,那么显然成本会改变事情。

    If ((condtionA || conditionB) == true) 那么 If-for 和 For-If 都是 1 pass。如果两个谓词都为真,那么显然您只想通过一次。

    doA 和 doB 无关紧要,因为我们假设它们在 If-for 和 For-If 中是相同的。

    如果谓词可以在评估过程中发生变化,那么必须考虑这一点。

    你问的是一个笼统的问题,所以答案笼统而模糊,没有更多细节。

    好的,现在您已经提供了附加信息(列表只有 5 个元素长,这是构建过程的一部分,谓词是静态布尔值)我们可以看到这里的瓶颈是 doA/B 函数。因此,您应该只循环一次。静态布尔检查可以忽略不计。

    【讨论】:

    • @SimeonVisser 请解释如何通过两次比一次更快。假设 (conditionA || conditionB) == true。
    • 这不是关于速度,而是关于正确性。根据doA 的行为,如果需要先执行所有doA 操作,则可能无法交织doAdoB 调用。
    • OP 询问哪个更有效。
    • @SimeonVisser 啊,我明白你关于 doA/doB 行为的观点 - 但你在做出 OP 没有提供的假设。
    • 您可以通过每次检查 a 和 b 来执行更多的检查操作。它是 O(n) 运行时复杂度。但实际操作的次数要多得多。
    【解决方案5】:

    就您进行多少比较而言,第二个更有效。

    检查条件a,1计算。 如果为真,则 Object.size 计算。

    检查条件b,1计算。 如果为真,Object.size 计算。 最小,2,最大对象.size * 2

    对于方法 2,您将始终执行 Object.size * 2 计算。

    如果两个检查都是错误的,请考虑您的“最坏情况”。方式 1 只会进行 2 次计算。方式 2 将执行 Object.size * 2。它与您的函数无关,因为在这两种情况下,在这两种情况下它总是需要相同的时间。

    即使在您的“最佳情况”中,如果两项检查都是正确的,您仍然会为 A 执行 N-1 次检查,为 B 执行 N-1 次检查。

    我认为用最少的计算来做到这一点的最佳方式。

    public void ifForMethod() {
        if (conditionA) {
            if(conditionB){
                for (Object o : listOfObjects) {
                    doA(o);
                    doB(o);
                }
            else{
                for (Object o : listOfObjects) {
                    doA(o);
                }    
            }
        }
        else if (conditionB) {
            for (Object o : listOfObjects) {
                doB(o);
            }
        }
    }
    

    您执行 2 次检查操作,然后最多只循环一次列表。

    【讨论】:

    • 当我添加更多条件时,这不会变得不可维护(有非常深的嵌套条件)吗?
    • 我试图提供以最少的计算量执行操作的方法。在这种情况下,代码本身的文件大小将比其他给定示例更大,但将以最少的最小/最大计算执行。确实,尽管您给出的两种情况都可以使用现代计算机,所以我想您想问的是理论而不是实践。这将执行与您的选项 2 相同的量级,但只会执行较少的计算。因此,如果您想成为最好的代码,并且不介意全部输入,请使用此代码,否则请使用您的 2 号代码以获取干净的代码
    【解决方案6】:

    这取决于! ifForMethod() 解决方案是最好的,如果存在条件A 和条件B 都不为真的真实情况。如果有条件A和条件B为真的情况,解决方案forIfMethod()是最好的,但是在进入循环之前应该预先计算条件。

    但您可以修改forIfMethod() 使其适用于所有情况:

    public void forIfMethod() {
      boolean a = conditionA;
      boolean b = conditionB;
      if (a || b) {
        for (Object o : listOfObjects) {
          if (a) {
            doA(o);
          }
          if (b) {
            doB(o);
          }
        }
      }
    }
    

    【讨论】:

    • 大约有十几个条件,平均 75% 是正确的。总会有至少一个是真的。
    【解决方案7】:

    让我们考虑我们在 for 循环和 if 内部都执行相同数量的操作。按照这个标准,我将采用第一种方法,在执行 for 循环之前使用 if 语句,以避免 for 循环中的迭代次数.

    另外,由于您正在使用高级 for 循环,与普通 for 循环相比,执行相同操作需要更多时间。

    如果我错了,请纠正我。

    【讨论】:

    • 特殊的 for 循环只是让代码更短的语法糖。不会影响性能。
    • 如果所有条件都为假则为真,那么当它们为真并且循环迭代多次时呢?
    【解决方案8】:

    这取决于您的代码试图解决的业务问题的性质。如果 conditionA 和 conditionB 都是简单的布尔变量而不是表达式,那么 For-If 会更好,因为您只在对象列表中循环一次。

    我们基本上是在比较哪个性能更好:多次从对象列表中枚举或多次评估布尔表达式。如果您的条件 A/条件 B 是非常复杂的布尔表达式,那么您的 If-For 将是一个更好的方法。

    【讨论】:

    • 条件本质上是简单的布尔表达式。它们比较复杂但每次都会返回相同的值,返回值可以保存并重复使用。
    • 如果条件结果可以重复使用,那么最好采用 for-if 方法,恕我直言,因为您只会在列表上迭代一次
    【解决方案9】:

    几件事:

    1. 这是过早的优化吗?一种方法是否比另一种更快真的很重要吗,这取决于数据,这可能不是人类明显的差异。
    2. 我会质疑软件的设计。为什么同一个对象有两种可能的条件?我建议将设计分解为多个对象。也许使用子类来执行不同的逻辑或使用策略模式。如果不更好地了解您在做什么,我无法更具体。

    【讨论】:

    • 1.如果我可以让它更快,我为什么不呢?这也每天发生数百万次。 2. 这是在复杂构建器对象的构建步骤中。
    • @Mike Amdahl's law。如果您需要花费大量额外时间来使整个程序运行得更快,那么您就不会使事情变得更快。我怀疑优化 do 方法会花费更多时间。
    【解决方案10】:

    第一个(if-for)对我来说听起来不错..因为对于第一种情况,将有一次检查整个 for 循环。但在第二种情况下,将检查每个循环。

    【讨论】:

    • 是的,但是多次遍历循环呢?
    猜你喜欢
    • 2018-04-15
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-09-09
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多