🔬 단순 카운트 테스트 (std::atomic vs mutex)

  • 스레드: 12개
  • 스레드당 카운팅: 5천만개

std::atomic

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
#include<iostream>
#include<iomanip>
#include<atomic>
#include<thread>
#include<mutex>
#include<chrono>
#include<vector>

const size_t THREAD_NUM = 12;
const size_t COUNT_NUM = 50'000'000;

std::atomic<int> atomic_counter(0);
void increse_counter_with_atomic(std::memory_order mo) {
    for(size_t i = 0; i < COUNT_NUM; i++) {
        atomic_counter.fetch_add(1, mo);
    }
}

int main() {
    std::vector<std::thread> threads;
    std::chrono::steady_clock::time_point start;
    std::chrono::steady_clock::time_point end;
    double ms = 0.0;

    // 스레드 생성 및 join
    start = std::chrono::steady_clock::now();
    for(size_t i = 0; i < THREAD_NUM; i++) {
        threads.emplace_back(increse_counter_with_atomic, std::memory_order_seq_cst);
    }

    for(std::thread &th : threads) {
        if(th.joinable()) {
            th.join();
        }
    }
    end = std::chrono::steady_clock::now();
    ms = (std::chrono::duration_cast<std::chrono::milliseconds>(end - start)).count();

    // 결과 출력
    std::cout << std::fixed << std::setprecision(3); // 소수점 아래 3자리 반올림
    std::cout << "duration: " << ms << "ms\n";
    std::cout << "count: " << atomic_counter.load() << "\n";

    return 0;
}
No.durationresult
15.563 s600000000
25.606 s600000000
35.616 s600000000
45.680 s600000000
55.636 s600000000

💡 mutex를 사용하지 않아도 thread-safety 확인 및 대략적으로 5.6초

mutex

  • 스레드: 12개
  • 스레드당 카운팅: 5천만개
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
#include<iostream>
#include<iomanip>
#include<atomic>
#include<thread>
#include<mutex>
#include<chrono>
#include<vector>

const size_t THREAD_NUM = 12;
const size_t COUNT_NUM = 50'000'000;

std::mutex mutex;
int mutex_counter = 0;
void increse_counter_with_mutex() {
    for(size_t i = 0; i < COUNT_NUM; i++) {
        mutex.lock();
        mutex_counter++;
        mutex.unlock();
    }
}

int main() {
    std::vector<std::thread> threads;
    std::chrono::steady_clock::time_point start;
    std::chrono::steady_clock::time_point end;
    double ms = 0.0;

    // 스레드 생성 및 join
    start = std::chrono::steady_clock::now();
    for(size_t i = 0; i < THREAD_NUM; i++) {
        threads.emplace_back(increse_counter_with_mutex);
    }

    for(std::thread &th : threads) {
        if(th.joinable()) {
            th.join();
        }
    }
    end = std::chrono::steady_clock::now();
    ms = (std::chrono::duration_cast<std::chrono::milliseconds>(end - start)).count();

    // 결과 출력
    std::cout << std::fixed << std::setprecision(3); // 소수점 아래 3자리 반올림
    std::cout << "duration: " << ms/1000 << "s\n";
    std::cout << "count: " << mutex_counter << "\n";

    return 0;
}
No.durationresult
132.837 s600000000
231.728 s600000000
332.469 s600000000
431.657 s600000000
533.651 s600000000

💡 5.6초 정도의 소요시간을 보인 std::atomic 과 많은 차이를 보이고, 다수의 스레드가 카운팅을 위해 lock 을 하는 비용이 너무 큰 것으로 보입니다.

✅ 결론

카운팅과 같은 작은 일에는 std::atomic 사용하여 오버헤드를 줄이는 것이 바람직하다.

🔬 Memory order 테스트

위 코드에서 메모리 오더만 변경해서 테스트 진행

memory_order_seq_cst

  • 위 내용과 동일

memory_order_relaxed

  • 28번 라인에서 memory_orderrelaxed 로 변경
No.durationresult
15.767 s600000000
25.575 s600000000
35.701 s600000000
45.671 s600000000
55.637 s600000000

💡 seq_cst 가 모든 스레드가 동일한 전역 순서를 공유한다고 하여 가장 느릴 것으로 예상했는데 relaxed 와 비슷한 수치를 보임

✅ 결론

  • 단순히 fetch_add 로 카운터만 올릴 때는 결과가 비슷함
  • 메모리 오더는 단순히 스레드간 메모리 가시성과 순서를 다룸
  • 복잡한 로직이라면 차이가 날 것으로 예상