2022-02-04-TIL
Today I Learned
테스트 코드 실행 범위
테스트 코드를 실행할 때 시간이 많이 걸린다면, 이미 검증이 완료된 부분을 다시 테스트하지 말고 변경한 부분과 그 영향을 받는 부분만 테스트하면 되지 않을까?
빌드 도구는 이미 일부를 지원한다. Gradle은 테스트 필터링으로 클래스 이름이나 메서드 이름 패턴에 맞는 테스트만 골라 실행할 수 있다. 버그를 고치거나 새 테스트를 만드는 중에는 이 방식으로 범위를 좁혀 빠르게 돌린다.
1
2
./gradlew test --tests 'com.example.order.*'
./gradlew test --tests OrderServiceTest.cancel
다만 “영향을 받는 부분”을 정확히 고르는 것이 어렵다. 변경한 클래스를 직접 쓰는 테스트는 찾기 쉽지만, 설정 파일, DB 스키마, 공통 유틸처럼 간접적으로 영향을 주는 변경은 의존 관계만으로 드러나지 않는다. 그래서 개발 중에는 좁은 범위로 자주 돌리고, 병합 전이나 CI에서는 전체를 돌리는 식으로 나누는 편이 안전하다.
레이스 컨디션과 코어, 스레드 수
다음 질문을 정리했다.
- 1코어 1스레드에서 레이스 컨디션이 일어나는가?
- 여러 개의 스레드면 일어나는가?
- 작업자가 여러 명일 때 떨어진 메모리에 접근하기 때문에 레이스 컨디션이 일어나는 것이다.
레이스 컨디션(race condition)은 실행 순서에 따라 결과가 달라지는 상황이다. 스레드가 하나뿐이면 실행 순서가 하나로 정해지므로 일어나지 않는다. 그런데 코어가 하나여도 스레드가 여러 개면 일어날 수 있다. 운영체제가 스레드를 번갈아 실행하다가 아무 지점에서나 다른 스레드로 전환하기 때문이다.
Java 튜토리얼의 Thread Interference는 c++ 같은 한 줄짜리 문장도 가상 머신에서는 값 읽기, 1 더하기, 다시 쓰기의 세 단계로 나뉜다고 설명한다. 두 스레드의 단계가 섞이면 한쪽 결과가 다른 쪽에 덮어써져 사라진다. 이 설명에는 코어 수가 등장하지 않는다. 단계 사이에 전환이 일어날 수 있느냐가 조건이다.
1
2
3
4
스레드 A: c 읽기 (0)
스레드 B: c 읽기 (0)
스레드 A: 1 더하기, c 쓰기 (1)
스레드 B: 1 빼기, c 쓰기 (-1) -> A의 결과가 사라진다
멀티 코어에서는 여기에 문제가 하나 더 붙는다. 각 코어가 자기 캐시를 가지고 있어서, 한 코어가 쓴 값이 다른 코어에서 언제 보이는지가 문제가 된다. “떨어진 메모리에 접근한다”는 메모는 이쪽에 가깝다. 정리하면 여러 스레드가 같은 데이터를 공유하고 그 접근이 원자적이지 않으면 코어 수와 상관없이 레이스 컨디션이 생기고, 멀티 코어는 캐시 때문에 가시성 문제를 더한다.
L1, L2, L3 캐시
CPU 캐시는 메인 메모리(DRAM)가 CPU보다 훨씬 느리기 때문에 둔다. Ulrich Drepper의 What Every Programmer Should Know About Memory는 캐시와 메인 메모리의 속도 차가 커지면서 L1보다 크고 느린 2단계 캐시가 추가되었고, 지금은 3단계 캐시를 가진 기계가 흔하다고 설명한다. L1은 다시 데이터용(L1d)과 명령어용(L1i)으로 나뉜다.
같은 문서에 실린 Intel Pentium M 기준 접근 비용은 다음과 같다. 오래된 CPU의 수치지만 계층 사이의 차이가 어느 정도인지 감을 잡기에는 충분하다.
| 위치 | 사이클 |
|---|---|
| 레지스터 | 1 이하 |
| L1d | 약 3 |
| L2 | 약 14 |
| 메인 메모리 | 약 240 |
캐시는 바이트 단위가 아니라 캐시 라인(보통 64바이트) 단위로 메모리를 가져온다. 그리고 멀티 프로세서에서는 모든 프로세서가 같은 메모리 내용을 보는 것처럼 캐시를 맞춰야 하는데, 문서는 이를 캐시 일관성(cache coherency)이라 부르고 대표 프로토콜로 MESI를 든다. 앞의 레이스 컨디션 이야기와 연결되는 지점이다.
- https://sanhee.notion.site/910d07be1b6142d0b327239e9974f509
댓글
아직 댓글이 없습니다