【问题标题】:C++ using precalculated limiters in for loopsC++ 在 for 循环中使用预先计算的限制器
【发布时间】:2010-03-01 17:44:16
【问题描述】:

在像 PHP 这样的脚本语言中,这样的 for 循环将是一个非常糟糕的主意:

string s("ABCDEFG");
int i;
for( i = 0; i < s.length(); i ++ )
{
   cout << s[ i ];
}

这是一个例子,我不是在构建这样的程序。 (对于那些觉得他们必须告诉我为什么这段代码在这里插入不好的地方>的人)

如果这个 C++ 示例被翻译成类似的 PHP 脚本,则每个循环周期都会计算字符串的长度。这会在现实脚本中造成巨大的性能损失。

我认为这同样适用于 C++ 程序,但当我查看教程、几个开源库和其他代码时,我发现循环的限制器没有预先计算。

  • 我应该预先计算字符串s的长度吗?
  • 为什么不总是预先计算限制器? (参见教程和示例)
  • 编译器是否进行了某种优化?

【问题讨论】:

  • 教程通常是为了可读性而不是速度而编写的。在示例中编写更清晰、更慢的代码并没有错。

标签: c++


【解决方案1】:

都是相对的。

PHP 被解释,但如果s.length 放入 PHP 解释器的编译部分,它不会很慢。不过就算慢了,在s[i]的时间呢,在cout &lt;&lt;的时间呢?

当被其他东西淹没时,很容易专注于循环开销。

如果你用 C++ 写这个,cout 写到控制台,你知道什么会占主导地位吗? cout 将遥遥无期,因为那个看似无辜的 &lt;&lt; 运算符调用了大量的库代码和系统例程。

【讨论】:

    【解决方案2】:

    你应该学会证明更简单的代码。试着说服自己迟早你会将 string::length 实现替换为更优化的实现。 (即使您的项目很可能会错过所有截止日期,优化 string::length 将是您的问题中最少的。)这种想法将帮助您专注于真正重要的事情,即使它并不总是那么容易......

    【讨论】:

      【解决方案3】:

      这取决于string 的实现方式。

      对于以空字符结尾的字符串,您必须在每次迭代时计算大小。

      std::string 是一个容器,应该在 O(1) 时间内返回,
      它取决于(再次)实现。

      【讨论】:

      【解决方案4】:

      优化器确实可以优化对length 的调用,如果他能够确定它的值不会改变的话——不过,如果你预先计算它,你是安全的(在许多情况下,然而, 优化将不可能,因为编译器不清楚条件变量是否可以在循环期间更改)。

      在许多情况下,这并不重要,因为所讨论的循环与性能无关。与for(int i=0,end=somewhat(); i &lt; end; ++i 相比,使用经典的for(int i=0; i &lt; somewhat(); ++i) 既省力又易于阅读。

      请注意,C++ 编译器通常会内联小函数,例如 length(通常会从字符串对象中检索预先计算的长度)。解释性脚本语言通常需要对函数调用进行字典查找,因此对于 C++,每次循环迭代的冗余检查的相对开销可能要小得多。

      【讨论】:

        【解决方案5】:

        你是对的,s.length() 通常会在每次循环迭代时进行评估。你最好写:

        size_t len = s.length();
        for (size_t i = 0; i < len; ++i) {
           ...
        }
        

        而不是上面的。也就是说,对于只有几次迭代的循环,调用 length() 的频率并不重要。

        【讨论】:

        • 注意:只有在你知道长度不能改变的情况下才这样做。
        • 嗯,你说的是std::string吗?因为我很确定std::string 将长度保留为字符串的属性,而不是每次都重新计算。
        • @mmyers:是的,它是一个“属性”,但它仍然是一个函数调用。编译器可能足够聪明,可以内联函数调用,但可能不会。编译器可以执行任一操作并且仍然符合标准。
        • @Billy:但它可能所做的实际上是非常无关紧要的,因为每个常见的编译器都会定期进行这种优化(当然,不同的规则适用于嵌入式系统;但是,它们总是做)。我们谈论的是微优化,而不是标准合规性。
        • “你最好……” 究竟如何?!在您测量对实际程序的影响之前,您甚至不需要浪费脑力周期来考虑这一点。
        【解决方案6】:

        我不知道php,但我知道c++ 做了什么。 考虑:

        std::string s("Rajendra");
        for (unsigned int i = 0; i < s.length(); i++)
        {
            std::cout << s[i] << std::endl;
        }
        

        如果您要查找 length() 的定义(右键单击 length() 并单击“转到定义”)或者如果您正在使用 Visual Assist X 则将光标放在 length() 上并按 @987654327 @,你会发现以下内容:

        size_type __CLR_OR_THIS_CALL length() const
            {   // return length of sequence
            return (_Mysize);
            }
        

        其中_Mysizeint 类型,这清楚地表明字符串的长度是预先计算的,并且每次调用length() 时只返回存储的值。

        但是,IMPO(在我个人看来),这种编码风格很糟糕,最好避免使用。我更喜欢以下:

        std::string s("Rajendra");
        int len = s.length();
        for (unsigned int i = 0; i < len; i++)
        {
            std::cout << s[i] << std::endl;
        }
        

        这样可以节省调用length()函数等于字符串长度的次数的开销,从而节省了栈帧的压入和出栈。当您的字符串很大时,这可能会非常昂贵。
        希望对您有所帮助。

        【讨论】:

          【解决方案7】:
          1. 可能。
          2. 为了便于阅读。
          3. 有时。这取决于检测循环内长度不会改变的能力。

          【讨论】:

            【解决方案8】:

            简短回答,因为在某些情况下您希望每次都调用它。

            别人的解释:http://bytes.com/topic/c/answers/212351-loop-condition-evaluation

            【讨论】:

              【解决方案9】:

              嗯 - 由于这是一种非常常见的情况,大多数编译器会预先计算该值。尤其是在遍历数组和非常常见的类型时 - 字符串可能就是其中之一。

              此外,引入额外的变量可能会破坏其他一些循环优化 - 它实际上取决于您使用的编译器,并且可能会因版本而异。
              因此,在某些情况下,“优化”可能适得其反。

              如果代码不是真正的“热点”,每一次性能表现都很重要,您应该像以前一样编写它:没有“手动”预先计算。

              在编写代码时,可读性也很重要!
              应该非常小心地进行优化,并且只能在深入分析之后进行!

              【讨论】:

                【解决方案10】:

                std::string.length() 返回固定变量,存储在容器中。已经预先计算好了

                【讨论】:

                • 是的,但编译器不一定要足够聪明才能内联函数调用。
                • 不一定预先计算:标准没有强制要求。但很有可能是这样。见stackoverflow.com/questions/256033/…
                • 那些标记为“(注 A)”的条目应该具有恒定的复杂性。
                【解决方案11】:

                在这种特殊情况下,std::string.length()通常(但不是necessarily)是一个恒定时间操作,通常非常有效。

                对于一般情况,循环终止条件可以是任何表达式,而不仅仅是循环索引变量的比较(实际上,C/C++ 不识别任何特定索引,只是一个初始化表达式、一个循环测试表达式和一个循环计数器表达式(每次都执行)。C/C++ for 循环基本上是do/while 复合语句的语法糖。

                【讨论】:

                  【解决方案12】:

                  编译器可能能够保存调用的结果并优化掉所有额外的函数调用,但它可能不会。但是,函数调用的成本将非常低,因为它所要做的就是返回一个 int。最重要的是,它很有可能会被内联,从而完全消除函数调用的成本。

                  但是,如果您真的很在意,您应该分析您的代码并查看预先计算值是否会使其更快。但是无论如何选择预先计算它并没有什么坏处。它不会花费你任何东西。不过,在大多数情况下,赔率并不重要。有一些容器 - 比如列表 - 其中 size() 可能不是 O(1) 操作,然后预先计算将是一个真的好主意,但对大多数人来说可能并不重要 -特别是如果您的循环内容足以压倒这种高效函数调用的成本。对于std::string,它应该是O(1),并且可能会被优化掉,但你必须测试才能确定——当然还有你编译的优化级别之类的东西at 可能会改变结果。

                  预先计算更安全,但通常没有必要。

                  【讨论】:

                    【解决方案13】:

                    std::sting::length() 返回一个预先计算的值。每次调用size() 方法时,其他 stl 容器都会重新计算它们的大小 例如

                    • std::list::size() 重新计算大小并
                    • std::vector::size() 返回一个 预先计算的值

                    这取决于容器的内部存储是如何实现的。 std::vector 是一个容量为 2^n 的数组,std::list 是一个链表。

                    【讨论】:

                      【解决方案14】:

                      只有当您知道字符串在循环内永远不会改变时,您才能预先计算字符串的长度。

                      我不知道为什么在教程中这样做。一些猜测:
                      1) 让您养成这种习惯,这样您在更改循环内字符串的值时就不会感到厌烦。
                      2)因为它更容易理解。

                      是的,优化器会尝试改进这一点,如果它可以确定字符串是否不会改变

                      【讨论】:

                      • 过早的优化是万恶之源。 /D 克努斯。 (顺便说一句,不是我的反对意见,尽管我可以同情这种情绪。)
                      • 是的,我同意这句话。我改写了我的答案,以便更清楚一点。
                      • @Pontus 也许,但这是你可以养成做的习惯而不必担心的事情之一,只要你不做任何会改变长度的事情细绳。你只是总是预先计算大小而忘记它。这样做不会花费您任何费用。这可以说是一个好习惯。不断担心它会很糟糕,但仅仅因为有人选择在这种情况下总是预先计算大小并不意味着他们做任何坏事。当预优化需要时间或涉及权衡时,这才是真正的问题。
                      • @Jonathan: ...但是,尽管最初编写代码所花费的时间微不足道,但过早优化的结果是代码稍微复杂一点,并且具有另一种故障模式。再加上有界数据结构上的 for 循环数量,这意味着可维护性会受到影响 - 有什么好处?
                      • @Pontus 如果您真的认为预先计算大小会降低可维护性(我不同意),那么您需要权衡取舍,这可能会使其不值得这样做。但是,在大多数情况下,您确实获得了潜在的性能提升,而没有性能成本,并且在某些情况下(列表),它几乎是一个有保证的增益(取决于实现),所以总是预先计算大小会减少多少你会不得不担心它。但是,如果您确实将其视为可维护性问题,那么它确实会成为一种权衡,并且可以被视为过早的优化并且不可取。
                      【解决方案15】:

                      仅供参考,在我的电脑上,g++ 4.4.2,带有-O3,使用给定的代码,函数std::string::length() const被调用了8次。

                      我同意它是预先计算的,并且该函数很可能是内联的。知道何时使用自己的函数/类仍然非常有趣。

                      【讨论】:

                        【解决方案16】:

                        如果您的程序一直对字符串执行大量操作,例如复制字符串(使用 memcpy),那么缓存字符串的长度是有意义的。

                        我在开源代码中看到的一个很好的例子是redis

                        sds.h(简单动态字符串)中查看 sdshdr 结构:

                        struct sdshdr {
                            long len;
                            long free;
                            char buf[];
                        }; 
                        

                        如您所见,它缓存了字符数组 buf 的长度(在 len 字段中)以及可用的可用空间(在 free 中> field ) 在空字符之后。

                        所以分配给buf的总内存是

                        len + free
                        

                        这样可以节省一个 realloc,以防需要修改 buf 并且它适合已经可用的空间。

                        【讨论】:

                          猜你喜欢
                          • 1970-01-01
                          • 2020-05-02
                          • 2014-03-30
                          • 1970-01-01
                          • 1970-01-01
                          • 1970-01-01
                          • 1970-01-01
                          • 1970-01-01
                          • 1970-01-01
                          相关资源
                          最近更新 更多