본문 바로가기

Computer Science/Computer Systems

[Lecture 13] 멀티쓰레딩 VII - Performance

#Performance Considerations

멀티스레딩 프로그램에서는 정확성이 가장 중요하다. 정확성은 성능과 바꿀 수 없다. 데이터 레이스, 원자성 위반, 순서 위반 또는 교착 상태가 발생하기 쉬운 코드의 성능에 대해서는 아무도 신경 쓰지 않는다.

 

그렇긴 하지만 이번 포스트에서는 멀티스레딩 프로그램에서의 성능에 대해서 알아보자. 먼저 잠금 (lock)의 비용을 알아 보자.

  • 간접 비용(잠금 사용으로 인한 성능 손실)
    • 아래 나올 사진들은 다음의 실험을 거처 나온 성능 결과이다.
    • 5개의 CPU 바인딩 프로세스를 시뮬레이션하여 Lock을 경합하고 D 기간 동안 각 잠금을 유지한 다음 U 기간 동안 잠금 없이 실행한다. 스레드가 임의로 잠금을 선택한다. 
    • 연한 녹색은 잠금 장치를 고정하지 않고 실행되는 쓰레드이다
    • 다른 색들은 lock을 가지고 있는 쓰레드이다.
  • 직접 비용(시스템이 이를 구현하기 위해 취해야 하는 조치와 관련됨)

#Indirect Cost: Loss of Parallelism Due To Single Lock

Figure 1: Fixed: U=2 / D=2 / L=1 / 40.8%

Figure 2: Fixed: U=18 / D=2 / L=1/ 96%

Figure 3: Poisson: U=2 / D=2 / L=1/ 41.6%

Figure 4: Poisson U=18 / D=2 / L=1/ 94.4%

#Indirect Cost: Loss of Parallelism with 2 Locks

Figure 1: Fixed: U=2 / D=2 / L=2 / 65.2%

Figure 2: Fixed: U=18 / D=2 / L=2 / 96%

Figure 3: Poisson: U=2 / D=2 / L=2 / 72.2%

Figure 4: Poisson U=18 / D=2 / L=2 / 99.4%

#Indirect Cost: Loss of Parallelism with 4 locks

Figure 1: Fixed: U=2 / D=2 / L=4 / 89.6%

Figure 2: Fixed: U=18 / D=2 / L=4 / 98%

Figure 3: Poisson: U=2 / D=2 / L=4 / 79.2%

Figure 4: Poisson U=18 / D=2 / L=4 / 99.4%

#Indirect Cost: Loss of Parallelism

  • 잠금으로 인한 직렬화로 CPU 활용률이 감소하고 개별 작업의 지연 시간이 늘어남
    • 병렬 애플리케이션(대부분 CPU 바인딩된 애플리케이션)의 경우 속도 향상의 손실로 직결됨
    • 특히 잠금 장치가 경합하는 경우(잠금 장치에서 나사산이 막히는 상황이 자주 발생한다)
    • 특히/스레드 차단 시 달리 실행할 것이 없는 경우
  • 차단된 스레드가 잠금(예: I/O, sleep, semait, pthread join)을 유지하는 경우 이러한 직렬화 효과는 악화될 수 있다.
  • 규칙: 중요 섹션은 차단될 수 있는 함수를 호출해서는 안 된다. 그렇지 않으면 중요 섹션에 액세스할 수 없게 될 수 있다
pthread_mutex_lock(&shutdownLock);
pthread_mutex_lock(&infoLock);
while (!moreInformation)
pthread_cond_wait(&moreInfo, &infoLock);
pthread_mutex_unlock(&infoLock);
pthread_mutex_unlock(&shutdownLock);

pthread_mutex_lock(&lock);
read(fd, buf, sizeof buf);
pthread_mutex_unlock(&lock);

#Solution: Breaking Up Locks

  • 주의 사항: 몇몇 대형 소프트웨어 시스템은 병렬화되지 않았거나 "Big Lock" 접근 방식으로 시작되었다: 리눅스 커널, 파이썬의 GIL, gtk GUI lock 등등
  • Lock L 보호 데이터(A, B, C)가 A, B, C를 보호하기 위해 Lock LA, LB, LC를 각각 도입한다
  • 따라서 A에 대한 업데이트는 B에 대한 동시 업데이트를 방해하지 않는다
  • 하지만 이로 인해 3가지 위험이 발생한다
    • 원자성 위반 위험 증가: A와 B를 동시에 업데이트해야 하는 경우(원자성) - 예를 들어 B에 대한 업데이트가 A에 값이 있는 경우, 두 잠금 장치를 모두 유지해야 한다. 항상 두 개의 잠금을 모두 유지하면 두 개의 잠금을 갖는 목적이 무효화된다. 필요한 곳에 두 잠금을 모두 유지하지 않으면 원자성 위반이 발생한다.
    • 교착 상태 위험 증가: 두 잠금 장치를 모두 고정해야 하는 상황이 발생할 경우 교착 상태를 방지하기 위해 잠금 순서를 설정해야 한다
    • 잠금/잠금 해제 호출이 잦아지면 직접 비용(잠금 오버헤드)이 증가한다

#Direct Cost of Locking

  • pthread mutex lock() 호출 시 내부에서는 어떤 일이 발생할까?
    • 빠른 경로: 원자 명령은 잠금이 사용 가능한지 여부를 나타내는 메모리 플래그에서 모드 스위치(예: cmpxchg %rax, (%rbx))를 유발하지 않고 잠금(사용 가능한 경우)을 획득하려고 시도한다
    • 느린 경로: 원자성 명령에 의해 잠금이 이미 보류되었다고 표시되면 시스템을 호출하고(futex wait), 커널에 스레드가 차단해야 한다고 알린다. 그런 다음 컨텍스트가 다른 준비 스레드로 전환된다(있는 경우)
  • pthread_mutex_unlock()
    • 빠른 경로: 잠금만 잠금 해제 상태로 설정
    • 느린 경로(누군가가 잠금을 기다리고 있음): 시스템을 호출하고(futex wake up) 커널에 대기 스레드를 깨우라고 알린다. 이러한 스레드는 차단 해제(준비됨)되어 준비 대기열에 배치되고 최종적으로 예약된다(다른 컨텍스트 스위치)
    • 모드 및 컨텍스트 스위치 모두 비용이 많이 들 수 있다(예: 파이프라인 stall, 캐시 오염).

#결론

  • lock의 최적화는 어렵다
  • 정확성이 무엇보다 중요하다
  • 성능에 미치는 영향을 예측하기 어려울 수 있음
  • 직렬화(serialization )를 줄이는 전략은 잠금 오버헤드를 증가시킬 수 있다
  • 일반적인 접근 방식은 거친 세분화된 잠금 전략으로 보수적으로 시작해야 하며, 반복적인 최적화 프로세스의 일부로 세분화된 잠금으로 이동해야 한다