【问题标题】:Switch statement with huge number of cases具有大量案例的 switch 语句
【发布时间】:2012-09-12 07:15:42
【问题描述】:

如果switch 有超过 5000 个case,会发生什么情况。有什么缺点以及我们如何才能用更快的东西来代替它?

注意:我不希望使用数组来存储案例,因为它是相同的。

【问题讨论】:

  • 为什么要比较 5000 个案例?难道你不能压缩/重构代码以减少每段代码的用例吗?
  • 您可以使用 函数指针数组,它比任何比较都快很多倍,但我的问题是...您如何管理和维护 包含 5000 个案例的 switch/case 语句的代码?
  • 如果您需要在 5000 个案例之间切换,这听起来像是糟糕的设计意图。否则,我也会选择函数指针。
  • @mmoment:重构绝对是解决方案,但我正在寻找其他方法。

标签: c++ c switch-statement


【解决方案1】:

除了 switch/case 语句之外,没有什么特别的理由认为您需要其他任何东西(而且我确实希望它没有帮助)。编译器应该创建有效的调度代码,这可能涉及静态[稀疏]表和直接索引、二进制分支等的某种组合;它可以深入了解案例的静态值,并且应该做得很好(每次更改案例时都会即时重新调整它,而新值不适合手工制作的方法 - 例如差异很大的值当您进行了非常紧凑的数组查找时 - 可能需要重新编写代码或默默地导致内存膨胀或性能下降)。

当 C 试图赢得铁杆汇编程序员的支持时,人们真的很关心这种事情……编译器负责生成好的代码。换句话说 - 如果它没有(可测量的)损坏,请不要修复它。

更一般地说,对这种事情感到好奇并了解人们对替代方案及其性能影响的想法是很棒的,但如果您真的关心并且性能差异可能会对您的程序(尤其是在分析建议时)然后始终以您的程序进行实际工作进行基准测试。

【讨论】:

  • 是的,我相信编译器会让 switch-case 生效,虽然 5000 个 case 是不正常的。可能函数指针数组更好,至少代码看起来会更好。
  • @Mine:函数指针数组将使代码可读。但我正在寻找一些更快的解决方案。
  • @Mine: 5000 例在典型的应用程序级代码中是不正常的,但是假设您正在编写编译器或操作系统 - 或者编译由某些自动化系统生成的代码,例如完美的哈希表生成器 - 你可以合理地期望这样的代码像涂了油的闪电一样运行。根据我对 Rusty 回答的评论,在运行时使用函数指针确实会对性能产生负面影响,因为最终会做一些微不足道的事情(例如设置/递增 int)的离线调用很容易成为一个数量级慢一点。
  • @TonyDelroy 如果他在每种情况下必须执行的操作真的很简单,那么使用汇编代码作为枚举值可能是他最好的解决方案(就像 BitBlt 函数的原始版本所做的那样)。如果他必须调用更复杂的函数,我不确定指令调度是否真的会成为他的问题。
  • 其中一个问题是编译器花费大量时间在单个文件中编译大量代码。
【解决方案2】:

值得深思……以防万一有人被旧的/错误的/低效的编译器困住,或者只是喜欢黑客攻击。

switch 语句的内部工作由两部分组成。找到要跳转的地址,然后跳转到那里。对于第一部分,您需要使用表格来查找地址。如果案例数量增加,表会变大 - 搜索地址跳转需要时间。这是编译器尝试优化的点,结合了多种技术,但一种简单的方法是直接使用表,这取决于大小写值空间。

在餐巾纸背面的例子;

switch (n) {
    case 1: foo(); break;
    case 2: bar(); break;
    case 3: baz(); break;
}

使用这样的代码编译器可以创建一个jump_addresses数组并直接通过array[n]获取地址。现在搜索只需要 O(1)。但是如果你有一个像下面这样的开关:

switch (n) {
    case 10: foo(); break;
    case 17: bar(); break;
    case 23: baz(); break;
    // and a lot other
}

编译器需要生成一个包含 case_id、jump_address 对和代码的表来搜索该结构,最差实现可能需要 O(n)。 (体面的编译器在完全释放这种情况时会优化地狱,通过启用优化标志到一定程度,当你需要调试这种优化的代码时,你的大脑会开始煎熬。)

那么问题是你能在 C 级别自己做这一切来击败编译器吗?有趣的是,在创建表格和搜索表格时似乎很容易,在标准 C 中无法使用goto 跳转到变量点。因此,如果您由于开销或代码不打算使用函数指针,则有可能结构,你被卡住了......好吧,如果你不使用GCC。 GCC 有一个名为Labels as Values 的非标准功能,它可以帮助您获取指向标签的指针。

要完成该示例,您可以编写具有“标签作为值”功能的第二个 switch 语句,如下所示:

const void *cases[] = {&&case_foo, &&case_bar, &&case_baz, ....};
goto *labels[n];
case_foo:
    foo();
    goto switch_end;
case_bar:
    bar();
    goto switch_end;
case_baz:
    baz();
    goto switch_end;
// and a lot other
switch_end:

当然,如果您谈论的是 5000 个案例,最好编写一段代码来为您创建此代码 - 这可能是维护此类软件的唯一方法。

作为结束语;这会改善您的日常工作吗?不会。这会提高你的技能吗?是的,从经验来看,我曾经发现自己仅通过优化案例值就改进了智能卡中的安全算法。这是一个奇怪的世界。

【讨论】:

  • 感谢您的回答,这不是我设计的一部分,但有人问我这个问题,我试图用函数指针数组来解释他。但这不是一个快速的方法。我自己在寻找更快的方法,我不是在这里谈论可读性或设计。
【解决方案3】:

尝试将 Dictionary 类与 Delegate 值一起使用。至少它使代码更具可读性。

【讨论】:

  • 该问题唯一明确的要求是“更快”......哈希映射可能会显着变慢,强制离线调用也可能会慢一个数量级(通常比单个琐碎操作的内联代码,这是切换速度的关键),尤其是在案例不是随机分布在大范围内的情况下。
【解决方案4】:

大 switch 语句,一般是自动生成的,可能需要很长时间才能编译。但我喜欢编译器优化 switch 语句的想法。

拆分 switch 语句的一种方法是使用分桶,

int getIt(int input)
{
  int bucket = input%16;
  switch(bucket)
  {
    case 1:
      return getItBucket1(input);
    case 2:
      return getItBucket2(input);
    ...
    ...
  }
  return -1;
}

所以在上面的代码中,我们将 switch 语句分成了 16 个部分。在自动生成的代码中更改存储桶的数量很容易。

此代码增加了一层间接或函数调用的运行时成本。 .但是考虑到不同文件中定义的桶,并行编译会更快。

【讨论】:

    猜你喜欢
    • 2010-10-23
    • 2014-01-03
    • 2021-03-22
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-06-19
    • 2016-04-13
    • 2015-06-21
    相关资源
    最近更新 更多