[33일차] | 컴퓨터 밑바닥의 비밀 | 책너두

Heechan Kang·2024년 5월 21일
0
post-thumbnail

5.3 다중 스레드 성능 방해자

5.3.1 캐시와 메모리 상호 작용의 기본 단위: 캐시 라인

  • 캐싱을 할 때, 하나의 단일 데이터만을 캐싱하는 것은 좋은 전략이 아니다.
    • 공간적 지역성의 원칙에 따라, 인접한 데이터를 함께 캐싱하는 것이 효율적이다.
    • 이를 캐시라인(cache line)이라고 한다.
      • 이 묶음은 일반적으로 64바이트이다.
      • 캐시가 미스되면 메모리에서 데이터를 읽을 때, 캐시 라인 단위로 읽어들인다.

5.3.2 첫 번째 성능 방해자: 캐시 튕김 문제

  • cpp 코드를 통해 캐시 튕김 문제를 확인해보자.

    • 첫 번째 코드

      atomic<int> a;
      
      void threadf()
      {
        for (int i = 0; i < 500000000; i++)
        {
          ++a;
        }
      }
      
      void run()
      {
        thread t1 = thread(threadf);
        thread t2 = thread(threadf);
      
        t1.join();
        t2.join();
      }
    • 두 번째 코드

      atomic<int> a;
      
      void run()
      {
        for (int i = 0; i < 1000000000; i++)
        {
          ++a;
        }
      }
  • 위 두 가지 코드를 비교하자면, 멀티코어에서 캐싱을 고려하지 않았을 때 첫번째 코드가 두번째 코드보다 두 배 빨라야 할 것처럼 보인다.

    • 1번 코드는 2개의 스레드에서 동시에 각 5억번, 2번 코드는 1개의 스레드에서 10억번을 실행하기 때문이다.
  • 그러나 실제 실행 시, 첫번째 코드의 실행시간은 16초, 두번째 코드의 실행시간은 8초로, 두번째 코드가 더 빠르다.

    • 이는 캐시 튕김(cache line bouncing, cache ping-pong) 문제로 인한 것이다.
    • 이는 두 스레드가 동시에 같은 변수(a)를 수정하므로, 서로 상대의 캐시를 무효화시키는 문제가 발생한다.

5.3.3 두 번째 성능 방해자: 거짓 공유 문제

  • 다음 두 코드를 통해 거짓 공유(false sharing) 문제를 확인해보자.

    • 전역변수

      struct data
      {
        int a;
        int b;
      }
      
      struct data global_data;
    • 첫 번째 코드

      void add_a()
      {
        for (int i = 0; i < 500000000; i++)
        {
          ++global_data.a;
        }
      }
      
      void add_b()
      {
        for (int i = 0; i < 500000000; i++)
        {
          ++global_data.b;
        }
      }
      
      void run()
      {
        thread t1 = thread(add_a);
        thread t2 = thread(add_b);
      
        t1.join();
        t2.join();
      }
    • 두 번째 코드

      void run()
      {
        for (int i = 0; i < 500000000; i++)
        {
          ++global_data.a;
        }
        for (int i = 0; i < 500000000; i++)
        {
          ++global_data.b;
        }
      }
  • 위 두가지 코드에서는 캐시 튕김 문제가 발생하지 않는다. 따라서 첫 번째 코드가 두 번째 코드보다 두 배 빠를 것으로 예상된다.

  • 그러나 실제로는 첫 번째 코드는 3초, 두 번째 코드는 2초로, 두 번째 코드가 더 빠르다.

    • 이는 거짓 공유(false sharing) 문제로 인한 것이다.

    • 두 스레드가 변수를 직접 공유하지는 않지만, 높은 확률로 두 변수가 같은 캐시라인에 있을 것이다. 따라서 이 때문에 캐시 무효화가 발생한다.

    • 이런 경우, 두 변수 사이에 사용하지 않는 변수를 채워넣으면 해결 될 수 있다.

      struct data
      {
        int a;
        int arr[16];
        int b;
      }
profile
안녕하세요

0개의 댓글