Deprecating volatile

По непонятным причинам я был уверен, что static volatile int является атомарным счетчиком. Вроде бы достаточно логично — оптимизация запрещена, чтение и запись должны быть сразу в оперативную память, работать все должно быстро. Как оказалось нет.

Одно из выступлений с cppcon, где один из разработчиков clang доступно объясняет, что вообще никто не знает что делает volatile:

Пришлось разобраться и проверить.

#include <stdio.h>
#include <thread>
#include <atomic>
 
std::atomic<int> acnt;
static volatile int vcnt;
const int ub_cnt = 0;
int cnt;
 
int f(int &lcnt, const int &local_const_cnt)
{
    for(int i = 0; i < 100000; i++) {
        ++cnt;
        ++acnt;
        ++vcnt;
        ++lcnt;
        ++(*const_cast<int*>(&ub_cnt)); // this will crash the program with -O0
        ++(*const_cast<int*>(&local_const_cnt));
    }
    return 0;
}
 
int main(void)
{
    int lcnt = 0;
    const int local_const_cnt = 0;
    
    std::thread* thr[1000];
    for(int i = 0; i < 1000; i++)
        thr[i] = new std::thread(f, std::ref(lcnt), std::ref(local_const_cnt));
    
    for (int i = 0; i < 1000; i++)
        thr[i]->join();
 
    printf("The atomic counter is %u\n", acnt.load());
    printf("The non-atomic counter is %u\n", cnt);
    printf("Volatile counter is %u\n", vcnt);
    printf("Local counter is %u\n", lcnt);
    printf("Local const counter is %u\n", local_const_cnt);
    printf("UB counter is %u\n", ub_cnt);
    
    // next line will crash the program because of undefined behaviour on any optimization level
    // printf("UB counter force load is %u\n", *const_cast<const volatile int*>(&ub_cnt));
}

Достаточно смешные результаты, плавающие от оптимизации (на -O0 ub_cnt я убрал):

vlad@vtyulb-thinkpad ~ % g++ main.cpp -lpthread -O0
vlad@vtyulb-thinkpad ~ % ./a.out
The atomic counter is 100000000
The non-atomic counter is 993844
Volatile counter is 912750
Local counter is 1815379
Local const counter is 0
UB counter is 0
./a.out  36,80s user 0,08s system 766% cpu 4,812 total
vlad@vtyulb-thinkpad ~ % ./a.out
The atomic counter is 100000000
The non-atomic counter is 1073648
Volatile counter is 938156
Local counter is 2031276
Local const counter is 0
UB counter is 0
./a.out  37,19s user 0,19s system 757% cpu 4,933 total


vlad@vtyulb-thinkpad ~ % g++ main.cpp -lpthread -O1
vlad@vtyulb-thinkpad ~ % ./a.out
The atomic counter is 100000000
The non-atomic counter is 27314903
Volatile counter is 1159589
Local counter is 36943434
Local const counter is 0
UB counter is 0
./a.out  36,77s user 0,14s system 764% cpu 4,829 total
vlad@vtyulb-thinkpad ~ % ./a.out
The atomic counter is 100000000
The non-atomic counter is 27600981
Volatile counter is 769927
Local counter is 35498305
Local const counter is 0
UB counter is 0
./a.out  38,26s user 0,08s system 763% cpu 5,019 total


vlad@vtyulb-thinkpad ~ % g++ main.cpp -lpthread -O2
vlad@vtyulb-thinkpad ~ % ./a.out
The atomic counter is 100000000
The non-atomic counter is 24814898
Volatile counter is 857348
Local counter is 52322802
Local const counter is 0
UB counter is 0
./a.out  41,11s user 0,10s system 765% cpu 5,385 total
vlad@vtyulb-thinkpad ~ % ./a.out
The atomic counter is 100000000
The non-atomic counter is 24785663
Volatile counter is 850257
Local counter is 51293639
Local const counter is 0
UB counter is 0
./a.out  40,76s user 0,13s system 767% cpu 5,329 total


vlad@vtyulb-thinkpad ~ % g++ main.cpp -lpthread -O3
vlad@vtyulb-thinkpad ~ % ./a.out
The atomic counter is 100000000
The non-atomic counter is 25486880
Volatile counter is 800002
Local counter is 55676774
Local const counter is 0
UB counter is 0
./a.out  39,96s user 0,11s system 761% cpu 5,263 total
vlad@vtyulb-thinkpad ~ % ./a.out
The atomic counter is 100000000
The non-atomic counter is 25704519
Volatile counter is 686848
Local counter is 54817634
Local const counter is 0
UB counter is 0
./a.out  39,46s user 0,15s system 757% cpu 5,229 total

Вообще, недавно прочитал почти весь блог одного товарища, начав с этой статьи http://scrutator.me/post/2015/04/05/cpu_memory_inter_multiprocessor.aspx
Это практически полностью перевернуло мое понимание многопоточного доступа к памяти, потому что раньше я считал барьеры памяти теоретической бесполезной фигней, а они оказывается действительно существуют: https://en.cppreference.com/w/cpp/atomic/atomic_thread_fence

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *