检查bool比调用空函数更有效吗?

时间:2014-02-24 01:58:23

标签: c++ performance function optimization polymorphism

如果我的某个对象Foo的函数Bar()可能包含或不包含任何实际功能,那么首先检查一个公共bool成员HasBar以确定是否存在一个点调用Bar()或只是调用该函数,如果它什么也不做就立即返回?

即。给出:

class Foo
{
public:
    bool HasBar;
    void Bar()
    {
    }
};

这样做更好:

Foo myFoo;
if (myFoo.HasBar)
{
    myFoo.Bar();
}

..或者这个:

Foo myFoo;
myFoo.Bar();

通常情况下,我认为后者会被编译器优化掉,但我们讨论的是由驻留在运行时动态加载的DLL的工厂动态实例化的对象。

由于我已经可以感觉到“过早优化”火炬和干草叉的临近,让我提供一些背景信息(除了我很想知道的事实,无论我在做什么):

这是其他应用程序运行的引擎的一部分,因此它越轻便越好(任何优化都是好的)。将会有一个多态Foo个对象(可能是数千个)的树,每秒迭代多次。每次迭代包括3次遍历树,并且在每次传递期间,在每个对象上调用不同的函数。

例如,在第一次传递期间,将在下一个Bar1()和最后一个Bar2()期间为每个对象调用Bar3()。有些对象正在这些功能中工作,有些则没有。迭代器不知道,因此它必须根据传递盲目地调用适当的函数。问题背后的原因是什么 - 它应该首先检查当前对象是否有意义地实现了相应的功能(例如通过检查每个对象暴露的标志),还是应该只是调用它?

4 个答案:

答案 0 :(得分:1)

如果没有进行优化,检查单个bool变量比进入函数便宜得多,在堆栈上分配东西并跳回。虽然这种优化还为时过早。

答案 1 :(得分:1)

这种方式应该没有太大的区别,因为你正在为有立即回报的电话交易条件。由于您的动态库是以任一方式链接的,因此无论您选择何种方式,都需要为链接付费。

使用布尔保护的方法可以节省一些CPU周期的唯一情况是当函数采用必须在调用之前评估的参数时,评估需要很长时间。

但是,使用guard的方法存在一个缺点:它引入了方法和布尔标志之间的耦合,因此无论如何实现它必须牢记这种连接。它在代码中并不明确,因此可以使用Bar的良好和长期实现来编写类,这不会仅仅因为您忘记将Foo切换为{{ 1}}。

很棒的是,这不是你需要事先决定的事情:编写代码并描述一个特别难以决定的方法。

答案 2 :(得分:1)

我测试了它,因为我很好奇。我不是C ++专家所以我只是测试了一些东西来混淆编译器,不要让他优化我的代码来破坏测试结果。我在使用Visual Studio 2012的Win8机器上进行了测试:

foo.h中

#pragma once
class Foo
{
   public:
      Foo(void);
      ~Foo(void);

      bool HasBar;
      int Bar();
};

Foo.cpp中

Foo::Foo(void)    {    }
Foo::~Foo(void)    {    }
int Foo::Bar()
{
   int a = 1, b = 1;
   return a + b;
} 

主:

int _tmain(int argc, _TCHAR* argv[])
{
{

    unsigned int start = clock();




    // call it because the bool is true
    for(int i = 0; i < 100000000; i++){

        bool trueBool;
        int sum = 0;
        int expected = 0;
        for(int j = i; j < 100 + i; j++){
            sum +=j;
            expected +=j;
        }
        trueBool = sum == expected;

        Foo f1;
        int returnedNumber;
        if(f1.HasBar){
                            // same code as in Bar()
            int a = 1, b = 1;
            returnedNumber = a + b;
        }
    }

    unsigned int end = clock();


    printf("call it because the bool is true: %d",   end - start);
}

{
    unsigned int start = clock();

    // dont call it
    for(int i = 0; i < 100000000; i++){

        bool trueBool;
        int sum = 0;
        int expected = 0;
        for(int j = i; j < 100 + i; j++){
            sum +=j;
            expected +=j;
        }
        trueBool = sum != expected;

        Foo f1;
        int returnedNumber;
        if(f1.HasBar){
                            // same code as in Bar()
            int a = 1, b = 1;
            returnedNumber = a + b;
        }

    }

    unsigned int end = clock();


    printf("dont call it: %d",   end - start);

}

{
    unsigned int start = clock();

    // call it anyway
    for(int i = 0; i < 100000000; i++){

        bool trueBool;
        int sum = 0;
        int expected = 0;
        for(int j = i; j < 100 + i; j++){
            sum +=j;
            expected +=j;
        }
        trueBool = sum == expected;

        Foo f1;
        int returnedNumber;
        returnedNumber = f1.Bar();

    }

    unsigned int end = clock();
    printf("call it anyway: %d",   end - start);
}

return 0;
}

结果:

调用它,因为bool为真:32446 不要叫它(因为bool是假的):32713 无论如何都要打电话:35224

如果bool为真或假,那么对函数的调用比调用稍慢一点。

订单并不重要。如果我将“将其调用” - 测试结果移至顶部,则会出现类似的测试结果。对函数调用的测试总是比bool的断言慢。

答案 3 :(得分:1)

如果可以避免大部分呼叫,则检查bool的速度会更快。检查bool比函数调用要快很多,但是如果以这种方式编写,代码就不那么优雅了。

但是,如果您需要在批处理中的所有Foo个对象上调用此函数,这可以让每个人都满意:

尝试使用预先分配的数组,该数组位于DLL内部,并使用标记为true的Foo对象填充(您甚至可能不需要再存储bool)。这可以在Foo个对象的一次传递中一次性填充。使HasBar对象的Foo的getter / setter从此容器中添加和删除可能是最优雅的。您必须以适合您代码的方式决定如何处理此细节。

现在,假设你有这样一个数组,你可以循环遍历它,在数组中的所有内容上调用Bar

这有许多优点:

  • 避免不必要的函数调用。
  • 避免不必要的bool检查。
  • 当您需要更改Foo的值时,仅由HasBar == true具有HasBar的对象进行缩放。
  • 使分支更具可预测性。
  • 如果将对象放在数组而不是指针中,那么引用的位置会更好。
  • 优雅且高效的代码。

你的情况更快吗?你必须尝试一下才能看到。