小是美丽的,但它也快吗?

时间:2010-11-24 09:19:26

标签: c++ c parsing coding-style

我与同事讨论过简单字符串解析器的实现。 一个是“小”,10行代码,使用c ++和流,另一行是70行代码, 使用switch case并通过char迭代字符串char。 我们测试了超过100万次迭代,并使用时间命令测量速度。 似乎漫长而丑陋的方法平均快了1秒。

问题: 输入:字符串

"v=spf1 mx include:_spf-a.microsoft.com include:_spf-b.microsoft.com include:_spf-c.microsoft.com include:_spf-ssg-a.microsoft.com ip4:131.107.115.212 ip4:131.107.115.215 ip4:131.107.115.214 ip4:205.248.106.64 ip4:205.248.106.30 ip4:205.248.106.32 ~all a:1.2.3.4"

输出: map<string, list<string>>包含每个键的所有值,例如:ip4,include,a

一个迭代的示例输出,在上面给出的输入字符串上:

键:一个

1.2.3.4,

键:包含

_spf-a.microsoft.com,_spf-b.microsoft.com,_spf-c.microsoft.com,_spf-ssg-a.microsoft.com,

键:IP4

131.107.115.212,131.107.115.215,131.107.115.214,205.248.106.64,205.248.106.30,205.248.106.32,

“小而美”的解析器:

        istringstream iss(input);
        map<string, list<string> > data;
        string item;
        string key;
        string value;

        size_t pos;
        while (iss.good()) {
                iss >> item;
                pos = item.find(":");
                key = item.substr(0,pos);
                data[key].push_back(item.substr(pos+1));
        }

第二种更快的方法:

  typedef enum {I,Include,IP,A,Other} State;
  State state = Other;
  string line = input;
  string value;
  map<string, list<string> > data;
  bool end = false;
  size_t pos = 0;
  while (pos < line.length()) {
   switch (state) {
    case Other:
     value.clear();
     switch (line[pos]) {
      case 'i':
       state = I;
       break;
      case 'a':
       state = A;
       break;
      default:
       while(line[pos]!=' ' && pos < line.length())
        pos++;
     }
     pos++;
     break;
    case I:
     switch (line[pos]) {
      case 'p':
       state = IP;
       break;
      case 'n':
       state = Include;
       break;
     }
     pos++;
     break;
    case IP:
     pos+=2;
     for (;line[pos]!=' ' && pos<line.length(); pos++) {
      value+=line[pos];
     }
     data["ip4"].push_back(value);
     state = Other;
     pos++;
     break;
    case Include:
     pos+=6;
     for (;line[pos]!=' ' && pos<line.length(); pos++) {
      value+=line[pos];
     }
     data["include"].push_back(value);
     state = Other;
     pos++;
     break;
    case A:
     if (line[pos]==' ')
      data["a"].push_back("a");
     else {
      pos++;
      for (;line[pos]!=' ' && pos<line.length(); pos++) {
       value+=line[pos];
      }
     }
     data["a"].push_back(value);
     state = Other;
     pos++;
     break;
   }
  }

我真的相信“小就是美丽”是要走的路,我不喜欢这里提出的更长的代码,但是当代码运行得更快时,很难争论它。

您是否可以建议一种优化或完全重写小方法的方法,在这种方法中,它可以保持小巧美观但运行速度更快?

更新: 添加了状态定义和初始化。 上下文:较长的方法在15.2秒内对同一个字符串完成100万次迭代,较小的代码平均在16.5秒内完成相同的操作。

使用g ++ -O3编译的两个版本,g ++ - 4.4, 在Intel(R)Core(TM)2 Duo CPU E8200 @ 2.66GHz,Linux Mint 10上运行

好的方面已经赢得了这场战斗:)我在小程序中发现了小错误,它甚至在地图中添加了无效值,那些没有字符串中的“:”冒号。 添加“if”语句以检查是否存在冒号后,较小的代码运行得更快,速度更快。现在的时间是:“小而美”:12.3又长又丑:15.2。

小是美丽的:)

11 个答案:

答案 0 :(得分:16)

小一些可能不会更快。一个例子:冒泡排序很短,但它是O(n * n)。 QuickSort和MergeSort更长,看起来更复杂,但它是O(n log n)。

但是说过......始终确保代码是可读的,或者如果逻辑很复杂,请添加好的注释,以便其他人可以关注。

答案 1 :(得分:14)

您拥有的代码行数较少;更好。如果你真的不需要,不要再增加60行。如果它很慢,那就是个人资料。然后优化。在您需要之前不要进行优化。如果运行正常,请保持原样。添加更多代码会增加更多错误。你不想要那个。保持简短。真。

阅读this wiki post

&#34;过早优化是所有邪恶的根源&#34; - Donald Knuth,一个非常聪明的人。

可以通过编写更少的代码来编写更快的代码,更智能。一种提高速度的方法:少花钱。

引用Raymond Chen:

&#34;我得到的一个问题是,&#34;我的应用程序启动缓慢。你们在微软用来让你的应用程序更快启动的超级秘密邪恶技巧是什么?&#34;答案是,&#34;超级邪恶的伎俩就是做更少的东西。&#34; - &#34;每个Win32程序员需要知道的五件事&#34; (2005年9月16日)

另外,请查看why GNU grep is fast

答案 2 :(得分:4)

第二个版本可能会更快,但也要注意它与正在阅读的内容高度相关,如果更改,你必须改变那些残酷的代码。第一个版本对此更加宽容,并且当某些内容发生变化时可能需要更少的维护。如果数据是垃圾/不正确,两者都会窒息。

真正需要问的问题是,额外的第二重要吗?有关系吗?

是的,寻找优化小/可读版本的方法。你可能会失去一秒钟,但你会立即明白。

答案 3 :(得分:2)

这是一个单一的功能,如果性能增益提供了显着的好处,我将保持丑陋的版本。 但正如其他人所提到的,记录得很好,并且可能在评论中包含小而美的版本,以便查看代码的人可以理解选择。

在这种特定情况下看起来选择对大型架构没有影响,它只是“叶子功能”的实现,所以如果有正当理由我不会保留缓慢的版本只是为了代码美学。

我很高兴知道当我调用某个库的Sort函数时它使用的是QuickSort而不是两行超级优雅但却很慢的BubbleSort: - )

答案 4 :(得分:2)

  

我真的相信“小就是美丽”是要走的路,我不喜欢这里提出的更长的代码,但是当代码运行得更快时,很难争论它。

不,不是。好吧,只要你说一个比另一个快一秒,你就意味着像一个需要10秒而另一个需要11秒而不是一个需要0.1秒而另一个需要1.1秒。即使这样,如果你只需要在程序启动时运行一次解析就可能不值得担心。

始终更喜欢简洁易懂的代码而不是长卷不透明但速度更快的版本,除非通过分析可以证明性能提升非常重要。别忘了程序员的时间也值得。如果你花费10分钟来试图弄清楚底部的代码是做什么的,那相当于节省了600次代码。

答案 5 :(得分:1)

一个小优化可能是为了避免使用地图[]运算符,而是首先使用find来查看地图中是否已存在密钥,否则使用{ {1}}添加新密钥。这两个insert也可以组合成一个,以避免不必要的复制。

另外,我不记得是谁首先说的,比快速代码更正更容易制作正确的代码。

Knuth关于过早优化的引用通常也是脱离背景的,他还说:

  

“传统智慧共享   今天的许多软件工程师   要求忽视效率   小;但我相信这只是一个   对他们看到的虐待行为反应过度   正在实践   小事精明,和芝麻丢了西瓜   程序员,无法调试或   维持他们的“优化”计划“

答案 6 :(得分:1)

考虑:

的大小
  1. 语言标准
  2. 使用的工具链
  3. 了解代码的必备知识
  4. 生成输出
  5. 编译时间
  6. 注意这些要点适用于在C ++编译器中实际编译C风格代码时使用的子集。

    重要的是要意识到,在没有你在“小”版本中使用的各种C ++标准库内容的隐含魔力的情况下,开始开发的时间可能会超过编写“长”版本所花费的时间。

    同样评论速度差异:大量使用库将消除编程能力的差异。要真正挤出表演和“美丽”,您需要掌握适用于该问题的最低级语言。

答案 7 :(得分:1)

在我为编写边界条件而编写的经验代码中,非常需要验证,是的,在没有行的情况下为代码库添加更多内容,但是非常需要制作生产就绪代码。

答案 8 :(得分:1)

我发现很难弄清楚第二个版本的功能。一开始,状态似乎没有初始化。

我也发现很难弄清楚第一个令牌没有冒号的地方。它似乎只是添加它无论如何没有价值?

第一个似乎是通用的,第二个专门用于了解您正在寻找的键。

如果性能就在这里并且您正在寻找特定的密钥,那么您可以通过各种方式进行优化,例如,只需要找到您正在寻找的命名列表,并了解它们是哪些。

然而,在你获得性能之前,如果这只是你只读了一次的配置,那么在一百万次迭代中慢一秒就会在现实中缩小一点并且不值得烦恼,而且一般性(而不是线条)它有的代码使它变得更好。

答案 9 :(得分:1)

我不同意答案,说你应该明确地选择较短,较慢的版本;取决于性能增益的折衷和改进的用户体验与代码的可读性和维护成本,更快的版本可能更可取。

相信你自己的判断!

关于这个特例,我有以下注意事项:

  • 这两个版本实际上并没有做同样的事情。较短的一个处理所有可能的密钥名称,而较长的一个只支持一些硬编码密钥。

  • 会猜测(这应该被分析!)两种算法花费的大部分时间都在std :: map和std :: list的构造/分配中节点。更改为不需要按键和按值分配的不同数据结构可能会大大加快这两种实现。

答案 10 :(得分:1)

我会用词法生成器对它进行破解。通过浏览输入,我猜大多数复杂性都等于确定令牌是什么。一个简单的标记化器和一个手写的状态机解析器(我猜它会有大约2或3个状态)应该总计大约20行代码:

extern string payload;

Output Process() {
   Output output;

   string key = "";
   bool value = false;
   while(!eof()) {
     switch(yylex()) {
      case tok_string:
       if (value) {
         output[key].push_back(payload);
         value = false;
         key = "";
       } else {
         key = payload;
       }
       break;

      case tok_colon:
       value = (key != "");
       break;
     }
   }
   return output;
}