我尝试创建一个模板来保存任意枚举类型(用于类型安全)并将其存储,如下面的代码段所示:
enum my_flags : uint8_t {
value = 0x01,
foo = 0x02,
bar = 0x04
}
template <class FlagType>
class atomic_flags {
FlagType fetch_and_set(FlagType f) {
//FlagType old; // <- Undefined behavior! At least in theory.
FlagType old = flag_.load(std::memory_order_relaxed); // Correct, but takes two times longer.
while(!flag_.compare_exchange_weak(old, static_cast<FlagType>(old | f))) {}
return old;
}
std::atomic<FlagType> flag_;
};
存储本身很简单,并不直接相关。我感兴趣的是两条注释线。第一个是C ++标准定义未定义行为(UB)。第二个是我应该用来正确的。但基准测试表明,它比第一个版本慢2倍。同时,第一个变体始终使用msvc编译器生成预期的行为。 (可能是因为编译器现在不必加载old
两次,因为这仍然由compare_exchange_weak
完成。)
现在我的问题:是否有可能在不依赖UB的情况下实现相同的性能?(是的,这是性能关键部分的一部分。)
作为旁注。如果我直接将uint8_t
替换为类型并使用fetch_or
的标准函数,则性能等同于UB情况。可能会尝试直接通过一个足以包含FlagType
的类型替换flag_
定义中的FlagType
,但这似乎对我很容易出错。
编辑:
这是我用于测试正确性和基准测试的代码(只有REQUIRE
语句将被遗漏在基准测试中。)
TEST_CASE( "Testing atomic_flags", "[atomic_flags]" ) {
enum my_enum : uint8 {
clear = 0x00,
first = 0x01,
second = 0x02,
third = 0x04,
fourth = 0x08,
fifth = 0x10,
all = first | second | third | fourth | fifth,
};
atomic_flags<my_enum> flag(clear);
REQUIRE(flag.fetch_and_set(first) == clear);
REQUIRE(flag.fetch_and_set(second) == first);
REQUIRE(flag.fetch_and_set(fifth) == (first | second));
REQUIRE(flag.fetch_and_set(third) == (first | second | fifth));
REQUIRE(flag.fetch_and_set(fourth) == (first | second | third | fifth));
REQUIRE(flag.fetch_and_clear(all) == all); // Note: fetch_and_clear removes a flag.
REQUIRE(flag.load() == clear);
}
我的基准测试结果是UB为40ns,每次调用为75ns。
答案 0 :(得分:2)
感谢大家快速帮助指出我可能出现的错误。
这两个版本的实际性能是等效的,但在第二种情况下,编译器由于某种原因没有进行所有可能的优化。
通过在fetch_and_set
语句中包含benchmark::DoNotOptimize()
函数的每个调用,两个案例都很好地形成(我正在使用google microbenchmark lib,此调用可以避免返回值被优化掉)。因此,原始问题的要点没有实际意义,初始化值显然是正确的选择。