这是一个错过的优化机会还是不是?

3

我发布了this的答案。代码:

#include <atomic>
#include <utility>
void printImpl(...);

std::atomic<bool> printLog = false;

class Log {
 public:
  template <typename T>
  const auto& operator<<(T&& t) {
    if (printLog) {
      ulog.active = true;
      return ulog << std::forward<T>(t);
    } else {
      ulog.active = false;
      return ulog;
    }
  }

 private:
  struct unchecked_log {
    template <typename T>
    const auto& operator<<(T&& t) const {
      if (active) {
        printImpl(std::forward<T>(t));
      }
      return *this;
    }
    bool active{false};
  };
  unchecked_log ulog{};
};

// Instead of the macro. Doesn't break backward compatibility
Log LOG;

void test(bool) { LOG << 1 << 2 << 3 << 4 << 5 << 6 << 7 << 8 << 9 << 10; }

本质上,代码要么忽略所有数据,要么记录所有数据。这个想法是将 atomic<bool> 记录在一个普通的 bool 中,以便更容易地进行优化。我认为大多数编译器可以轻松地优化掉 if (active) 部分,因为它在调用之间没有改变的方式。结果发现,大多数编译器确实内联了对 unchecked_log::operator<<函数的调用,但并未优化掉分支。是否有什么东西阻止了这种优化?这样做是否合法?


2
Log::operator << 依赖于全局变量 printLog。如果你真的不能改变它,编译器会期望使用常量。而且这是同一个 ulog 对象,我认为标准可能会禁止 ulog::operator << 的这种“优化”,例如如果有两个线程怎么办? - Fire Lancer
1个回答

2

LOG是一个具有外部链接的全局变量。因此,另一个翻译单元中printImpl的定义可以访问它,并且在调用之间可能修改LOG.ulog.active

LOG变成test中的局部变量,这样重复的检查就会在进入test时合并为一个检查,或者保留LOG在原地并使其成为static,以便包含printImpl定义的不同编译单元无法访问该翻译单元的实例。

如下评论所述,或者让operator<<返回一个副本,这样它返回的实例(现在是临时的)对于printImpl来说是无法访问的。


请注意,ulogulog.active的可访问性(private等)并不重要。只要printImpl能够获得指向相关实例的指针或引用,private就无法保护免受修改。以下是一些可能的情况示例(非详尽):

  1. 调用LOG上的operator<<,它可能会根据printLog的干预修改LOG.ulog.active或通过const_cast结果进行修改
  2. 调用默认的复制/移动赋值运算符
  3. (由于这是一个标准布局类)将LOG重新解释为其ulog成员的引用
  4. (由于类是可平凡复制的)将不同状态的memcpyLOG
  5. 将新的Log实例放置在LOG中,这将使以前的引用自动引用新对象,因为Log满足该条件
  6. 等等。

@walnut 我认为 reinterpret_cast 方法不会起作用,因为 Log::unchecked_log 是一个私有类,所以无法在 reinterpret_cast<Log::unchecked_log*>(&LOG) 中命名它,因此 const_cast 方法是唯一可以防止优化的方法。 - Ayxan Haqverdili
如果您返回值或使用没有外部链接的 LOG,那么 printImpl 就无法获取与实例相关的引用,因此它不能使用。这些假设是针对您继续使用具有外部链接的全局 LOG.ulog 的情况而言的。因此,优化是有效的。 - walnut
1
@Ayxan 是的,printImpl 可以修改 LOG.ulog.active,但这不是你在 test 中一直使用的。第一个 << 返回另一个 unchecked_log 实例(按值返回),然后在 LOG << ... 表达式中作为临时对象显式,并在其上调用其他 <<。如果您没有明确地将引用传递给它,printImpl 将无法访问此临时实例。 - walnut
@JesperJuhl 当我有一对函数,其中一个接受const指针并返回另一个相关的const指针,另一个接受非const指针并返回类似的非const指针时,我会写const_cast。非const函数调用const函数,并将结果进行const转换。没有伤害小猫。 - Martin Bonner supports Monica
1
@Ayxan 注意,由于您在构造返回值之前首先调用了printImpl,因此实际上会有两个分支(请参见汇编代码)。这并不是完全最优的。为了使其最优化,您应该首先复制ulog,然后稍后返回它,或者将ulog完全局部化到Logoperator<<重载中。 - walnut
显示剩余9条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接