【问题标题】:Will gcc optimize away repeated function calls upon the same variable with same output for each call?gcc 是否会优化对同一变量的重复函数调用,每次调用具有相同的输出?
【发布时间】:2014-08-22 17:34:42
【问题描述】:

对于一个应用程序,我的情况是相同的信息以多种形式存在:Base64 字符串、十六进制字符串和char[]

现在为了提高效率,我不是煞费苦心地为每个函数声明和初始化一个变量,而是仅在上述形式之间的明显转换点应用它。之所以如此,是因为有些点不需要将变量转换为另一种形式进行条件比较等操作。

根据我的阅读,编译器的效率似乎令人难以置信,而且一天比一天高;然而,当我尝试阅读更深入的分析和描述时,我经常会超过我的经验极限,我的大脑就会溢出。

如果一个函数被重复调用单个变量以将其更改为另一种形式,例如从 Base64 字符串到每次产生相同结果的十六进制字符串,编译器是否会优化这些调用,以便为整个范围是不必要的?

在我的情况下,我使用-Ofast 直到有更好的东西。

【问题讨论】:

    标签: c++ gcc optimization compiler-construction


    【解决方案1】:

    编译器可以优化的内容实际上取决于代码的编写方式;然而,依赖过于聪明的编译器通常是不明智的。编译器擅长优化寄存器分配和各种低级的东西,但是如果你知道你的程序中有一些不变量可以更有效地编写代码,不要假设编译器理解整个程序.

    对于您提到的这个特定示例,如果您将数据包装在一个实现各种格式的转换运算符并缓存转换结果的类中,这将比依赖编译器不重做更好的方法同样的计算。但是,如果将这些转换运算符标记为“const”,则编译器可能会重用先前调用“const”方法的结果(假设没有执行交错的非常量操作)。但是,除了缓存结果之外,我建议您这样做,而不是依赖此优化。

    此外,当涉及到这些优化时,唯一确定的方法是使用特定编译器实际编译代码并检查程序集输出以确定它是否应用了该优化。

    【讨论】:

    • 哈哈,呵呵!您的意思是 union 之类的东西,在我的情况下,这 3 种形式是合二为一的?请理解,我在此时您可以想象的最基本级别上编写 c++ 代码,并且当我不得不做任何可能被认为是危险的事情时会畏缩。非常感谢您的快速和翔实的回答!你介意给一个菜鸟看一个简单的例子吗?非常感谢您!
    • 我实际上并不是指一个联合(一个联合一次只能保存一个值),而是一个具有不同格式的“可变”指针的类,其中 -- 在初始化时-- 只有一个是非空的,但其他的是在转换时计算和缓存的。 (并且,在分配时,其他变体无效)。
    • 嗯,当你使用像“指针”这样的危险词时,考虑到我的经验和智商,这绝对是我不应该玩的地方。我想我大体上明白你在说什么:有一个通用的class,它在初始化时会创建所有三种类型并相应地引用它们。在一个菜鸟/不是那么高的智商水平上是正确的吗?再次感谢您分享您的出色知识!
    • 这基本上是正确的,只是它会延迟初始化指针(即在转换时),在初始化时只初始化其中一个。
    【解决方案2】:

    我希望gcc 不会执行那种优化。如果是这样,则必须满足几个要求,例如同时编译被调用的函数——以提供跨调用优化寄存器的可能性。

    这样的优化令人印象深刻,但并非纯粹有用。程序员可以轻松编写对函数的调用并保存返回值。

    CDC Cyber​​ (c. 1975) FORTRAN 编译器有有趣的行为。它将优化对IRAND() 的调用。这让不少写游戏的学生感到惊讶和困惑,比如掷两个骰子的 sn-p:

    integer roll
    roll = mod (irand(0), 6)  +  mod (irand(0), 6)  +  2
    

    这只会产生偶数,因为它认为它好像是写出来的

    roll = 2 * mod (irand(0), 6)  +  2
    

    它在 1978 年左右被报告为一个错误,该错误已通过未优化涉及 irand()rand() 的表达式得到修复。通过使优化变得更加困难,这很容易解决:

    integer roll, die1, die2
    die1 = mod (irand(0), 6)  +  1
    die2 = mod (irand(0), 6)  +  1
    roll = die1 + die2
    

    只要优化不是太高,它就会按预期工作。第一个示例始终优化:无法关闭。

    【讨论】:

    • 这是一个非常有趣的警告!谢谢!
    【解决方案3】:

    这里有一些代码来说明这个概念:

    class TriString
    {
      public:
        enum Format { Binary, Hex, Base64 };
    
        TriString(const std::string& s) : s_(s) { }
    
        // mutators - must modify b_ and h_ accordingly or clear them
    
        TriString& operator=(const std::string& rhs)
            { s_ = rhs; b_.clear(); h_.clear(); }
    
        TriString& erase(size_type index = 0, size_type count = npos)
        {
            s_.erase(index, npos);
            h_.clear(); // will need regeneration...
            b_.erase(index * 2, count == npos ? npos : count * 2);
        }
    
        char& operator[](size_type n)
        {
            h_.clear();
            b_.clear();
            return s_[n];
        }
    
        // ...add more as needed...
    
        // accessors
    
        const std::string& get(Format) const
        {
            if (Format == Binary || s_.empty())
                return s_;
            if (Format == Hex)
            {
                if (h_.empty()) h_ = to_hex(s_);
                return h_;
            }
            // Format == Base64
            if (b_.empty()) b_ = to_base64(s_);
            return b_;
        }
    
        const char& operator[](size_type n) const { return s_[n]; }
    
        // ...add more as needed...
    
      private:
        std::string s_;          // normal string
    
        // "cached" conversions - invariant: valid if not empty(), or s_.empty() too
        // (mutable so get(Format) const can modify despite being const)
        mutable std::string b_;  // base64 encoded
        mutable std::string h_;  // hex encoded
    };
    

    使用通常的std::string 接口执行此操作并不安全,因为如下客户端代码不起作用:

    TriState s("hello!");
    char& c = s[2];
    const std::string& h = s.get(TriState::Hex);  // triggers caching of hex conversion
    c = 'x';                                      // oops - modifies s_ without clearing/updating h_
    const std::string& h2 = s.get(TriState::Hex); // oops - gets old cached h_ despite changed s_
    

    您必须做出一些选择来限制接口以避免授予持续更改字符串的能力(如非const operator[]、迭代器等),返回可以清除的代理对象(而不是例如字符引用)写入时缓存的转换,或记录客户端使用的一些限制并希望最好....

    【讨论】:

    • 哇,这太极端了!太感谢了!你刚刚给了我一个扩展我的经验的模板!
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-08-17
    • 2020-11-26
    相关资源
    最近更新 更多