책너두 (컴퓨터 밑바닥의 비밀) 33일차 다중 스레드 성능 방해자

  • CPU가 4바이트 정수에 접근할 때, 캐시 히트를 하지 못하면 간단히 캐시에 4바이트 정수를 저장하면 된다고 생각함.
    • 그러나 그렇지 않음.
  • 캐시와 메모리 상호 작용의 기본 단위: 캐시 라인
  • 프로그램이 어떤 데이터에 접근하면 다음에는 인접한 데이터에 접근할 가능성이 높으므로 접근해야 할 데이터만 캐시에 저장하지 않음.
    • 해당 데이터가 있는 곳의 ‘묶음’ 데이터를 캐시에 저장함.
    • 이를 캐시 라인 이라고 함.
  • 묶음 데이터는 일반적으로 64바이트임.
  • 캐시와 메모리 사이의 상호 작용에 대한 세부 내용은 다중 스레드 프로그래밍이고, 일반인이라면 생각해 낼 수 없음. → 매우 흥미로운 문제를 가져옴.
  • 첫 번째 성능 방행자: 캐시 튕김 문제
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;
	}
}
  • 위 프로그램은 스레드 2개를 이용해 전역변수 a를 1씩 5억번 증가시킴.
  • 아래 프로그램은 단일 스레드로 전역변수 a를 1씩 10억번 증가시킴
  • 다중 코어 컴퓨터 기준, 첫 번째 프로그램 실행 시간은 16초, 두번 째 프로그램 실행 시간은 8초였음.
    • 왜 스레드 2개쓴 프로그램이 오히려 더 느릴까?
  • 리눅스 perf 도구를 이용하여 두 프로그램을 분석하면 ‘insn per cycle’ 항목을 보자.
    • 해당 항목은 하나의 클럭 주기에 CPU가 실행하는 프로그램에서 기계 명령어를 몇 개 실행하는지 알려줌.
    • 다중 스레드 프로그램은 insn per cycle 이 0.15로, 하나의 클럭 주기 동안 기계 명령어가 0.15개 실행 되었음.
    • 단일 스레드 프로그램은 0.6으로 다중 스레드 프로그램의 네배에 달함.
  • 참고로, a 변수를 원자적 변수가 아닌 int 형으로 정의하면 실행 시간은 더욱 빨라짐.
  • 이렇게 다중 스레드 프로그램의 성능이 좋지 않은 이유가 무엇일까?
    • 캐시 일관성을 보장하기 위해, 두 코어의 캐시에 전역 변수 a 처럼 저장 되어야 함.
    • ex) 캐시 1에서 a 변수를 1로 업데이트 하면 캐시 2의 a 변수를 무효화(invalidation) 해야 함. → 여기서 첫 번째 캐시 튕김이 발생함.
    • 캐시 2는 캐시 무효화가 되었기에 어쩔 수 없이 메모리에서 직접 a 변수 값을 읽어야 함.
      • 근데 이때 또, a 변수 값을 1을 더해야 하므로 캐시 일관성을 보장하기 위해 이번에는 캐시 1의 a 변수를 무효화 해야 함.
    • 이렇게 서로 끊임 없이 상대 캐시를 무효화하면서 튕겨 냄.
      • 이를 캐시 튕김(cache line bouncing) 또는 캐시 핑퐁(cache ping-poing) 문제임.
    • 즉, 여러 스레드 사이에 데이터 공유를 피할 수 있다면 가능한 한 피해야 함.

댓글

Designed by JB FACTORY