使用指向成员或交换机的指针数组会更好吗?

时间:2015-05-05 19:40:24

标签: c++ optimization switch-statement function-pointers

在我的学校,我们强烈建议在C ++(和C)中使用指向成员的指针数组而不是switch(或多个if)。

由于我没有看到使用这些数组的任何意义(我实际上使用指向成员的指针的映射)而不是switch语句,我想知道是否有任何类型的优化会推荐指向函数的指针

这就是让我觉得使用开关更好的原因:

  • 指向成员的指针的数组(特别是map)是内存繁重的(std :: string作为键,指针作为值),需要存储在类中(没有任何意义,因为它是不是对象属性...)或者在函数中每次使用它们重新创建(如果是静态声明的话):

    std::map<std::string, void (MyClass::*)(...)>   operations;
    
  • 实例化并准备好使用它们很痛苦:

    operations.insert(std::map<std::string, void (Parser::*)(std::vector<std::string> const &)>::value_type("push", &Parser::push));
    operations.insert(std::map<std::string, void (Parser::*)(std::vector<std::string> const &)>::value_type("pop", &Parser::pop));
    operations.insert(std::map<std::string, void (Parser::*)(std::vector<std::string> const &)>::value_type("dump", &Parser::dump));
    operations.insert(std::map<std::string, void (Parser::*)(std::vector<std::string> const &)>::value_type("assert", &Parser::assert));
    operations.insert(std::map<std::string, void (Parser::*)(std::vector<std::string> const &)>::value_type("add", &Parser::add));
    operations.insert(std::map<std::string, void (Parser::*)(std::vector<std::string> const &)>::value_type("sub", &Parser::sub));
    operations.insert(std::map<std::string, void (Parser::*)(std::vector<std::string> const &)>::value_type("mul", &Parser::mul));
    operations.insert(std::map<std::string, void (Parser::*)(std::vector<std::string> const &)>::value_type("div", &Parser::div));
    operations.insert(std::map<std::string, void (Parser::*)(std::vector<std::string> const &)>::value_type("mod", &Parser::pop));
    operations.insert(std::map<std::string, void (Parser::*)(std::vector<std::string> const &)>::value_type("print", &Parser::print));
    operations.insert(std::map<std::string, void (Parser::*)(std::vector<std::string> const &)>::value_type("exit", &Parser::exit));
    
  • 它会强制你在某些函数中使用无用的参数,并拥有可能是const的非const成员。例如,在我之前的代码中,“print”和“assert”可能是const,如果它们没有在地图中使用,并且大多数函数不使用参数,而是“push”和“assert”是...

  • 您必须验证您要使用的指针是否存在于地图中,而不是让“默认”案例处理它,并且调用很难阅读:

    if (operations.find(myOperation) != operations.end())
        (this->*(operations.find(myOperation)->second))(myParameter);
    

那么为什么我们被迫使用指向成员的指针而不仅仅是一个明确的switch语句,或者甚至是else-ifs?

感谢。

3 个答案:

答案 0 :(得分:4)

这取决于。具有多个未连接选项的Switch-case实际上与big if-else有效 - 慢。好的优化是使用建议您实现的偏移表(或跳转表)执行所需的操作。

奇怪的是,编译器通常可以自动执行这种优化 - 如果switch-case写得很好。

写得好意味着什么?

这意味着,您必须设计条目索引,因此计算需要执行的条目的位置将变得简单快捷。请考虑以下代码:

int n = 0;
std::cin >> n;

if(n == 1) printf("1\n");
else if(n == 2) printf("2\n");
else if(n == 3) printf("3\n");
else if(n == 4) printf("4\n");

这是可能的输出(VC11上的实际输出,用/ O2编译):

011AA799  mov         eax,dword ptr [n]  
011AA79C  cmp         eax,1 //is n equal to 1?
011AA79F  jne         main+34h (011AA7B4h) //if yes, continue, if not, jump... [J1]
011AA7A1  push        1262658h  
011AA7A6  call        printf (011E1540h) // print 1
011AA7AB  add         esp,4  
011AA7AE  xor         eax,eax  
011AA7B0  mov         esp,ebp  
011AA7B2  pop         ebp  
011AA7B3  ret  
011AA7B4  cmp         eax,2 // [J1] ...here. Is n equal to 2?
011AA7B7  jne         main+4Ch (011AA7CCh) //If yes, continue, if not, jump... [J2]
011AA7B9  push        126265Ch  
011AA7BE  call        printf (011E1540h) // print 2
011AA7C3  add         esp,4  
011AA7C6  xor         eax,eax  
011AA7C8  mov         esp,ebp  
011AA7CA  pop         ebp  
011AA7CB  ret  
011AA7CC  cmp         eax,3 // [J2] ...here. Is n equal to 3? (and so on...)
011AA7CF  jne         main+64h (011AA7E4h)  
011AA7D1  push        1262660h  
011AA7D6  call        printf (011E1540h)
[...]

基本上 - if-else。现在,让我们改变我们的代码:

int n = 0;
std::cin >> n;

switch(n)
{
case  1: printf("1\n"); break;
case  2: printf("2\n"); break;
case  3: printf("3\n"); break;
case  4: printf("4\n"); break;
}

可能的输出:

011BA799  mov         eax,dword ptr [n]  // switch case will run if n is 1-4
011BA79C  dec         eax //decrement by one, now it should be in 0-3
011BA79D  cmp         eax,3 // compare with 3
011BA7A0  ja          $LN4+46h (011BA7EFh) //if greater than 3, skip switch
011BA7A2  jmp         dword ptr [eax*4+11BA7F8h] //otherwise compute offset of instrcution and jump there

我没有向printf发送电话 - 基本上相同,但没有任何cmp或跳转说明。

此输出当然只是众多可能中的一种,但重点是:设计良好的应用程序,对条件部分进行智能优化,可以提高效率。在这里,编译器能够直接跳转到正确的指令,因为它可以轻松计算其偏移量 - 所有情况都用数字标记,增长为1。

更直接地回答你的问题:给出的建议在技术上是正确的,但不是复杂的代码(可能会或可能不会显着提高速度),我会专注于编译器友好的优化,而不是每个人都能理解并且依赖(就编译器足够聪明以利用这一优势并生成优化代码而言)。

答案 1 :(得分:3)

与开关指令相比,您对成员函数指针数组的优缺点的分析已经非常好了。

但这一切都取决于具体情况:

  • 从技术上讲,当然,你是完全正确的:如果你只想更换一个开关,这样的阵列非常麻烦。没有谈到可以使用jump tables来优化交换机的编译器,它使用比数组少一个间接的方法。

  • 但您的示例代码实现了一种 command design pattern 。从设计的角度来看,这可以在渐进性和可维护性方面具有很大的优势,从而超越了技术缺陷。例如,它可以很容易地在应用程序中用于实现撤消/重做功能。它还简化了几个同时用户界面允许在对象上触发这些命令的情况(例如:命令行窗口和GUI)

答案 2 :(得分:0)

背景很重要。

如果您使用PC,我认为阵列是首选,因为与非常比较相比,获得结果的速度非常快,购买带内存的付费。 这在内存中很昂贵,但阵列非常快。

如果上下文是微控制器,则内存非常昂贵,您无法浪费来保存所有阵列。特别是如果几乎不使用该阵列。 但是可以优先选择开关,因为没有使用内存,而且汇编器在微控制器中非常快。

  • 如果你有这么多的内存和高级编程语言,那么阵列可能更好。
  • 如果内存很少,汇编程序的编程语言很少,或者微软的C语言,那么切换(或rom表)就更好了