编译器优化是否解决了线程安全问题

Are compiler optimization solving thread safety issues?

本文关键字:线程 安全 问题 解决 优化 是否 编译器      更新时间:2023-10-16

我正在编写一个C++多线程代码。在测试不同互斥锁的开销时,我发现线程不安全的代码似乎能在Visual Studio中使用Release Configuration编译出正确的结果,但比使用互斥锁的代码快得多。然而,通过调试配置,结果是我所期望的。我想知道是编译器解决了这个问题,还是只是因为在Release配置中编译的代码运行得太快,以至于两个线程永远不会同时访问内存?

我的测试代码粘贴如下。

class Mutex {
public:
unsigned long long  _data;
bool tryLock() {
return mtx.try_lock();
}
inline void Lock() {
mtx.lock();
}
inline void Unlock() {
mtx.unlock();
}
void safeSet(const unsigned long long &data) {
Lock();
_data = data;
Unlock();
}
Mutex& operator++ () {
Lock();
_data++;
Unlock();
return (*this);
}
Mutex operator++(int) {
Mutex tmp = (*this);
Lock();
_data++;
Unlock();
return tmp;
}
Mutex() {
_data = 0;
}
private:
std::mutex mtx;
Mutex(Mutex& cpy) {
_data = cpy._data;
}
}val;
static DWORD64 val_unsafe = 0;
DWORD WINAPI safeThreads(LPVOID lParam) {
for (int i = 0; i < 655360;i++) {
++val;
}
return 0;
}
DWORD WINAPI unsafeThreads(LPVOID lParam) {
for (int i = 0; i < 655360; i++) {
val_unsafe++;
}
return 0;
}
int main()
{
val._data = 0;
vector<HANDLE> hThreads;
LARGE_INTEGER freq, time1, time2;
QueryPerformanceFrequency(&freq);
QueryPerformanceCounter(&time1);
for (int i = 0; i < 32; i++) {
hThreads.push_back( CreateThread(0, 0, safeThreads, 0, 0, 0));
}
for each(HANDLE handle in hThreads)
{
WaitForSingleObject(handle, INFINITE);
}
QueryPerformanceCounter(&time2);
cout<<time2.QuadPart - time1.QuadPart<<endl;
hThreads.clear();
QueryPerformanceCounter(&time1);
for (int i = 0; i < 32; i++) {
hThreads.push_back(CreateThread(0, 0, unsafeThreads, 0, 0, 0));
}
for each(HANDLE handle in hThreads)
{
WaitForSingleObject(handle, INFINITE);
}
QueryPerformanceCounter(&time2);
cout << time2.QuadPart - time1.QuadPart << endl;
hThreads.clear();
cout << val._data << endl << val_unsafe<<endl;
cout << freq.QuadPart << endl;
return 0;
}

标准不允许您假设代码默认情况下是线程安全的。在x64的发布模式下编译时,您的代码仍然会给出正确的结果。

但为什么

如果您查看为代码生成的汇编文件,您会发现优化器只是简单地展开循环并应用常量传播。因此,它不是循环65535次,而是在计数器中添加一个常量:

?unsafeThreads@@YAKPEAX@Z PROC              ; unsafeThreads, COMDAT
; 74   :    for (int i = 0; i < 655360; i++) {
add QWORD PTR ?val_unsafe@@3_KA, 655360 ; 000a0000H   <======= HERE 
; 75   :        val_unsafe++;
; 76   :    }
; 77   :    return 0;
xor eax, eax                             
; 78   : }

在这种情况下,每个线程中只有一条非常快速的指令,就不太可能发生数据竞赛:很可能一个线程在下一个线程启动之前就已经完成了。

如何查看基准测试的预期结果

如果希望避免优化器展开测试循环,则需要将_dataunsafe_val声明为volatile。然后您会注意到,由于数据竞争,不安全的值不再正确。用这个修改后的代码运行我自己的测试,我得到了安全版本的正确值,而不安全版本的值总是不同的(错误的)。例如:

safe time:5672583
unsafe time:145092                   // <=== much faster
val:20971520
val_unsafe:3874844                   // <=== OUCH !!!!
freq: 2597654

想要使您的不安全代码安全吗

如果您想在不使用显式互斥的情况下确保不安全代码的安全,您可以将unsafe_val设置为atomic。结果将取决于平台(实现很可能会为您引入互斥),但在与上面相同的机器上,MSVC15处于发布模式,我得到:

safe time:5616282
unsafe time:798851                    // still much faster (6 to 7 times in average)
val:20971520
val_unsafe:20971520                   // but always correct
freq2597654

然后您还必须做的唯一一件事是:将变量的原子版本从unsafe_val重命名为also_safe_val;-)