ITE4065 · HANYANG UNIV · 2017 FALL

병렬 프로그래밍

야심은 상위 5%, 검증은 하위 50%. 네 프로젝트 모두 “돌아간다”를 “정확하다”의 증거로 받아들였고, 병렬 프로그래밍은 그 둘이 가장 크게 어긋나는 분야다.

추적 파일24,565
커밋359
기간2017.09–12
스택C++14 / 17
B-종합
소견12 치명적11 중대3 경미합계 26

총평

네 개의 프로젝트 중 Project 4는 학부 과제로서 드문 수준이다. MariaDB 10.2의 InnoDB 락 매니저에 lock-free 리스트, 논리/물리 삭제 분리, 타임스탬프 기반 메모리 회수를 직접 이식했고, 설계 다이어그램과 perf 전후 비교, 원본 sysbench 로그까지 첨부했다. 난이도와 깊이만 보면 대학원 프로젝트에 가깝다.

그러나 네 프로젝트가 동일한 실패 패턴을 공유한다. 정확성의 증거를 남기지 않았다는 것이다. Project 1의 제출 브랜치에는 스레드가 없고, Project 2는 데드락 탐지 로직 전체가 조기 return 한 줄에 막혀 실행되지 않으며, Project 3의 성능 그래프는 스냅샷 처리량이 아니라 태스크 큐 push 횟수를 그린 것이고, Project 4의 성능 향상은 상당 부분 락 충돌 검사를 제거해서 얻은 것이다.

병렬 프로그래밍은 테스트 통과가 정확성의 증거가 되지 못하는 거의 유일한 분야다. 이 레포는 그 사실을 배우기 직전에서 멈춰 있다. 다만 README마다 한계를 솔직히 기록해 두었고, Project 3에서 발견한 레이스를 스스로 고친 커밋이 남아 있으며, Project 4에서는 실제로 perf로 병목을 프로파일링했다. 한 학기 안에 검증 습관이 자라는 궤적이 커밋 히스토리에 그대로 보인다.

프로젝트주제핵심 판정등급
P1Signal (다중 패턴 매칭)제출 브랜치에 병렬성 없음 + 알고리즘 오표기C+
P2Two-Phase Locking + RW Lock데드락 탐지가 dead code, 자랑한 뮤텍스가 미사용C
P3Wait-Free Snapshot알고리즘 골격은 정확, atomic 부재 + 벤치마크 무효B-
P4Scalable Lock Manager깊이는 최고, 락 의미론을 제거해서 얻은 성능B+

Project 1 — Signal

패턴 집합에 대해 질의 문자열의 부분 일치를 찾고, 실행 중 패턴 추가(A)와 삭제(D)를 지원하는 과제. 저자는 Aho-Corasick 자동자를 표방했다.

치명적

제출 브랜치(master)에 스레드가 0개다

project1/src/main.cc는 순수 싱글스레드다. include/pool.h에 스레드풀이 존재하지만 main.cc 어디서도 사용되지 않는다. 병렬 프로그래밍 과목의 첫 과제에서 기본 브랜치가 순차 코드인 것은 변명의 여지가 없다. origin/multi 브랜치에 병렬 버전이 있으나, 채점자가 master를 본다면 그것은 없는 것과 같다.

project1/README.md· 저자 본인의 기록
- [ ] Thread pool (not in this version)
    - [ ] minimize cond lock
    - [ ] find optimized thread size
치명적

Aho-Corasick이 아니다

README는 - [x] make Failure Link and Output Link로 체크되어 있지만, include/ahocorasick.h 어디에도 failure link를 구성하는 코드가 없다. match()는 모든 시작 위치마다 루트에서 트라이를 다시 타는 완전 탐색이다. 복잡도는 Aho-Corasick의 O(|q| + matches)가 아니라 O(|q| × Lmax)로, 실체는 “트라이로 최적화한 브루트포스”다. 이름과 문서가 구현을 잘못 광고하고 있다.

project1/include/ahocorasick.h:45–68
// 시작 위치마다 state_init에서 다시 출발 — failure link가 없다
for (start = 0; start < query.length(); start++) {
    string r = "";
    state = state_init;
    pos = start;
    do {
        r += query[pos];
        state = raw[state][query[pos++] - CHAR_START];
        if (state < state_init && state > -1) {
            auto iter = find(result.begin(), result.end(), r); // O(k) 중복검사
            if (iter == result.end()) result.push_back(r);
        }
    } while ((state != -1) && (pos < query.length()));
}
중대

패턴 갱신이 전체 재구축이다

sync()(ahocorasick.h:102)는 A/D 이후 첫 질의에서 자동자 전체를 다시 만든다. 저자 본인의 주석에 It's not necessary, and slow라고 적혀 있고, README의 - [ ] efficent delete algorithm도 미완 표시다. 즉 이 과제에서 난이도가 몰려 있는 부분이 미해결 상태다.

경미

쓰이지 않는 헬퍼에 남은 UB

include/unique.h:211unique::list::_insert(const T&)return true가 없다. 반환값을 사용하면 정의되지 않은 동작이다. 정작 이 클래스는 어디에서도 사용되지 않는다(dead code). 결함 자체보다 쓰지도 않을 제네릭 컨테이너를 직접 만들었다는 사실이 이 레포 전체의 우선순위 문제를 대변한다.

중대

multi 브랜치는 병렬화 축이 틀렸다

질의 하나를 DEFAULT_THREAD_SIZE(17)개로 쪼개 fork-join 한다. 질의 길이가 수십~수백 바이트면 packaged_task 할당 + 뮤텍스 큐 + future::get 비용이 매칭 본체보다 크다. 정석은 질의 파이프라인(입력 / 매칭 / 출력 순서 재배열)이다. 게다가 failure link가 없는 알고리즘을 그대로 병렬화했으므로, 잘못된 것을 빠르게 하는 구조다.

Project 2 — Two-Phase Locking

N개 스레드가 R개 레코드에 대해 읽기 1회·쓰기 2회로 구성된 트랜잭션을 수행하고, Readers-Writer 락과 2PL로 직렬성을 보장하며 데드락을 탐지해 abort하는 과제. 네 프로젝트 중 가장 상태가 나쁘다.

치명적

데드락 탐지가 실행되지 않는다

330행의 return true 때문에 그 아래 wait-for 그래프 구성과 BFS 사이클 탐지 전체가 도달 불가능한 코드다. 실제 동작은 “데드락 탐지”가 아니라 “충돌하면 무조건 abort”다. 과제의 핵심 요구사항이 구현되어 있지 않다.

project2/include/container.hpp:328–350
bool assert_deadlock(Operation* request) {
    if (waiting[request->record_id()].empty()) return false;
    return true;                       // ← 330행

    // 아래는 전부 실행되지 않는다
    for (const auto& wait : waiting[request->record_id()]) { ... }
    while (!qupend.empty()) { ... }       // 사이클 탐지 BFS
}
치명적

연쇄 결과 — README가 자랑한 RW 뮤텍스가 한 번도 대기하지 않는다

락 보유자는 항상 waiting[rid]에 엔트리를 남긴다. 따라서 try_acquire 실패는 곧 waiting[rid]가 비어 있지 않음을 뜻하고, 위 결함에 의해 모든 충돌이 예외 없이 abort된다. 그 결과 블로킹 acquire()로 가는 try_failed 분기도 도달 불가능해진다.

즉 README가 가장 길게 설명한 산출물 — 획득 순서를 보장하는 thread::safe::Mutexlock() / lock_shared() 대기 로직 — 이 프로그램 수명 동안 단 한 번도 실행되지 않는다. 실제로 쓰이는 것은 try_lock / try_lock_shared뿐이다. 노력의 대부분이 죽은 코드에 들어갔다.

치명적

그 뮤텍스 자체도 깨져 있다

try_lock()(mutex.hpp:50)은 FIFO 순서를 무시하고 waiter뒤에 자신을 넣으면서 writing = true로 만든다. 반면 unlock()은 “첫 번째 writer 엔트리”를 지운다. 따라서 대기 중인 다른 writer의 엔트리를 지울 수 있고, 그 writer는 영원히 만족될 수 없는 조건을 기다린다.

또한 do { wait(lock, pred); } while (writing); 구조는, wait(lock, pred)가 술어가 참이면 즉시 반환한다는 점에서 위험하다. 이 상태에 빠지면 내부 mutex를 쥔 채 tight spin이 되고, unlock()이 그 뮤텍스를 얻지 못해 영구 정지한다.

project2/include/mutex.hpp:36–42, 50–62
waiter.emplace_back(true, tid);
do {
    writer.wait(lock, [&w = this->waiter, tid](){
        return w.front() == std::make_pair(true, tid);
    });
} while (writing);              // pred가 참이면 wait이 즉시 반환 → 스핀

bool try_lock(size_t tid = 0) {
    if (writing || 0 < reader_count) return false;
    waiter.emplace_back(true, tid);  // 큐 맨 뒤에 넣고 락은 지금 획득
    writing = true; return true;
}
중대

abort 경로마다 새는 메모리와 남는 dangling pointer

container.hpp:167–169에서 첫 연산이 abort될 때 new로 만든 Operation 3개를 해제하지 않는다. 위에서 보았듯 모든 충돌이 abort이므로 상시 누수다.

더 나쁜 것은 overflow abort 경로다. waiting[]에 등록된 포인터를 제거하지 않은 채 undo()가 그 객체를 delete한다. waiting해제된 포인터가 영구히 남고, 그 레코드를 건드리는 이후 모든 트랜잭션은 영원히 abort한다.

중대

커밋 카운터가 초기화되지 않았고 락 없이 읽힌다

container.hpp:313T count;는 생성자 초기화 리스트에 없고 기본 멤버 초기자도 없다. 커밋 ID와 종료 조건(order() > e)이 미정의 값에서 출발한다. 게다가 order()global 뮤텍스 없이 count를 읽어 데이터 레이스다.

project2/include/container.hpp:311–316
std::mutex global;

T count;                             // 초기화 없음
std::vector<record*> records;
std::vector<std::set<size_t>> depend;
중대

설계 차원의 아이러니 — 전역 뮤텍스

transaction()의 모든 락 획득·해제·커밋이 단 하나의 std::mutex global 안에서 일어난다. per-record RW 락을 만든 의미가 사라지고 사실상 전역 직렬화다. Project 4에서 저자가 비판한 lock_sys->mutex 병목을 Project 2에서 스스로 재현한 셈이다.

경미

정확성의 증거가 레포에 없다

정답 검증 스크립트 test.py는 동료(Sangchul Lee)의 것이고 — 출처를 명시한 것은 옳다 — 실행 결과가 하나도 커밋되어 있지 않다. 이 프로젝트가 옳게 동작했다는 증거가 레포에 존재하지 않는다. 또한 표준 라이브러리 분기를 #if defined(__GNUC__) && (__GNUC__ < 7)로 했는데, clang은 __GNUC__ = 4를 정의한다. 실제로 현재 빌드하면 fatal error: 'experimental/string_view' file not found로 죽는다. __has_include를 봤어야 한다.

Project 3 — Wait-Free Snapshot

N개 스레드가 각자의 레지스터를 갱신하면서, 어떤 스레드도 다른 스레드를 기다리지 않고(wait-free) 전체 배열의 일관된 스냅샷을 얻는 자료구조. 알고리즘 골격은 Herlihy–Shavit의 표준 구성을 정확히 따랐다. 문제는 그 아래층이다.

치명적

atomic::Snapshotstd::atomic이 없다

이 알고리즘의 정확성 증명은 “레지스터의 읽기/쓰기가 원자적이고 순서가 보인다”를 전제한다. 코드에는 그 전제가 없다. x86에서 정렬된 포인터 저장이 찢어지지는 않지만, 메모리 배리어도 volatile도 없어 -O3에서 컴파일러가 객체 초기화와 저장을 재배열하거나 table[i] 로드를 루프 밖으로 끌어낼 수 있다. C++ 표준상으로는 그냥 data race, 즉 정의되지 않은 동작이다. 네임스페이스가 atomic인데 원자 연산이 0개다.

project3/include/snapshot.hpp:117–121, 78–82
std::valarray<StampedSnap*> table;   // 평범한 포인터 배열
std::valarray<bool> moved;           // 전 스레드 공유 (아래 참조)
std::valarray<std::queue<StampedSnap*>> trash;

// update() 안 — 평범한 저장, 배리어 없음
StampedSnap* new_value = new StampedSnap(value, old_value->get_stamp() + 1, snap);
table[tid] = new_value;
치명적

moved 배열이 스레드 간 공유다

원 알고리즘에서 moved[]각 scan 호출의 지역 상태다. 여기서는 Snapshot의 멤버라 모든 스레드가 공유한다. 두 가지가 동시에 깨진다.

  • 다른 스레드가 scan을 시작하며 fill로 내 진행 상태를 리셋한다 → 최대 n회 재시도라는 wait-free 보장이 무너진다.
  • 다른 스레드가 세워둔 moved[i]를 보고 “i가 두 번 움직였다”고 오판한다 → 내 scan 시작보다 이전의 스냅샷을 정답으로 반환한다. 선형화 지점이 존재하지 않는 값을 돌려주는 것이므로 정확성 결함이다.
project3/include/snapshot.hpp:88–100 (scan 내부)
std::valarray<StampedSnap*> old_value = table;
std::fill(std::begin(moved), std::end(moved), false);  // 공유 상태를 리셋
...
if (old_value[i]->get_stamp() != new_value[i]->get_stamp()) {
    if (moved[i]) return old_value[i]->get_snap();  // 남의 플래그일 수 있음
    else { flag = moved[i] = true; old_value = new_value; break; }
}
치명적

벤치마크가 스냅샷 성능을 측정하지 않는다

출력되는 숫자는 메인 스레드가 큐에 넣은 태스크 수이지 완료된 update 수가 아니다. 게다가 모든 future를 60초 내내 보관한다.

직접 빌드해 돌린 결과(g++ -O2, 4스레드, 2초): update : 2161742, maximum resident set size 368,967,680 바이트. 60초로 환산하면 10GB급이다. 따라서 results/out_SCS의 56M, out_server2의 98M도 전부 push 카운트이고, result.png는 사실상 “단일 생산자 + 뮤텍스 하나짜리 태스크 큐”의 처리량 그래프다.

project3/src/main.cc:39–46
while (!pool.is_stop()) {
    tasks.emplace(pool.push([&snapshot, v=random.next()](size_t tid) {
        snapshot.update(tid, v);
    }));                          // future를 전부 보관 → 메모리 폭증
}
std::cout << "update : " << tasks.size() << '\n';  // 완료 수가 아니라 push 수
중대

“왜 코어 많은 서버가 더 느린가”의 답

README는 “I do not understand why SCSLab Server's return low performance than others”로 끝난다. 원인은 다섯 가지가 겹친 것이며, 전부 코드에서 확인된다.

원인내용
단일 생산자모든 update가 메인 스레드 하나를 통과한다. 상한이 고정된다.
queue_mutex 1개스레드가 늘수록 이 뮤텍스 경합만 증가. 1→32스레드에서 56M→10M으로 감소한 이유다.
update당 O(n) 할당scan()이 valarray를 2회 복사하고 new StampedSnap이 스냅샷을 또 복사한다 → 할당자 경합.
false sharing8바이트 포인터 배열 tablemoved가 몇 개의 캐시라인에 몰려 있다. 매 update가 전원의 캐시를 무효화한다.
단일코어 속도E5-2697 2.7GHz vs i7-7700K 4.2GHz. 이 워크로드는 사실상 단일코어 벤치마크다.

올바른 측정은 스레드마다 자기 루프에서 update를 돌리고 스레드-로컬 카운터를 합산하는 것이다. 코드 20줄이면 바뀌고, 그랬다면 물음표로 끝나지 않았을 것이다.

중대

메모리 회수가 “1000개면 괜찮겠지”다

trash[tid]에 1000개가 쌓이면 delete한다(snapshot.hpp:75). 그 순간 다른 스레드의 scan()old_value[i]로 그 포인터를 들고 있을 수 있다 → use-after-free. hazard pointer나 epoch reclamation이 필요한 자리를 상수 지연으로 대체했고, 주석에도 근거 없이 I think do not need라고만 적혀 있다. 초당 100만 회 갱신에서 1000회는 1밀리초도 되지 않는 유예다.

잘한 것

초기 버전은 태스크 람다 안에서 공유 std::mt19937을 호출했다(명백한 레이스). 이후 v=random.next()로 메인 스레드에서 미리 뽑아 캡처하도록 고친 커밋이 남아 있다. 레이스를 스스로 발견하고 고친 이력은 이 레포에서 가장 좋은 신호다.

Project 4 — Scalable Lock Manager

MariaDB 10.2 InnoDB의 락 매니저는 lock_sys->mutex 하나로 보호되어 코어 수가 늘면 병목이 된다. 이를 latch-free 설계로 바꿔 확장성을 확보하는 과제. 난이도와 야심 면에서 네 개 중 압도적이며, 설계 다이어그램·perf 전후 비교·원본 로그까지 갖춘 유일한 프로젝트다. 그러나 성능 향상의 상당 부분이 락을 걸지 않아서 나왔다.

치명적

충돌 검사가 존재하지 않는다

lock_rec_lock_fast가 항상 새 락을 만들고 성공을 반환하므로, 충돌 검사(lock_rec_other_has_conflicting)·대기 큐 등록·데드락 탐지를 담당하는 lock_rec_lock_slow절대 호출되지 않는다. 저자 본인의 주석에도 그렇게 적혀 있다. lock_set_lock_and_trx_wait#ifndef ITE4065로 제거되어 어떤 트랜잭션도 락을 기다리지 않는다.

storage/innobase/lock/lock0lock.cc:2612–2618, 2755–2757
RecLock rec_lock(index, block, heap_no, mode);
trx_mutex_enter(trx);
rec_lock.create(NULL, trx, true, true);      // 무조건 생성
trx_mutex_exit(trx);
status = LOCK_REC_SUCCESS_CREATED;           // 항상 성공

//Jiun: Atomic lock append with no-fail. so, lock_rec_lock_slow will never call
치명적

락 객체가 “어떤 레코드”인지 모른다

lock_allocut_malloc_nokey(sizeof(lock_t))만 할당하고, n_bits 설정과 lock_rec_set_nth_bit(lock, rec_id.m_heap_no)를 건너뛴다. InnoDB는 레코드 비트맵으로 락 대상을 식별하는데 그 비트맵이 없다. 충돌 검사를 하고 싶어도 정보가 없어 불가능하다.

따라서 이것은 “확장 가능한 락 매니저”가 아니라 “락을 걸지 않는 락 매니저”다. read-only 워크로드는 S락끼리 어차피 충돌하지 않으므로 Milestone 1의 결과는 우연히 맞지만, Milestone 2(read/write)는 격리 수준이 사실상 사라진다. README의 “I ran the sysbench test to write to it, but it was not a problem”은 증거가 되지 못한다 — sysbench oltp_read_write는 쿼리 성공 여부만 보고 직렬성을 검증하지 않는다. 바로 다음 줄의 “I am expecting that someday it'll get an error”가 정확한 진단이다.

치명적

힙 오버플로우 — 포인터 크기만 할당하고 구조체로 쓴다

sizeof(hash_cell_t*)는 8바이트인데 hash_cell_t{node, tail, head}로 24바이트다. 16바이트를 힙 밖에 쓴다.

storage/innobase/lock/lock0lock.cc:509–512
//Jiun: Initialize gclist
lock_sys->gclist = static_cast<hash_cell_t*>(ut_malloc_nokey(sizeof(hash_cell_t*)));
lock_sys->gclist->head = NULL;     // offset 16 — 할당 범위 밖
lock_sys->gclist->tail = lock_sys->gclist->head;
치명적

GC가 사실상 동작하지 않는다 — 타임스탬프 부여 시점 버그

물리 삭제는 실행 중 트랜잭션의 최소 타임스탬프를 구해 cur_lock->timestamp < min_timestamp인 락만 해제한다. 그런데 ACTIVE 트랜잭션의 timestamp는 항상 0이다. 타임스탬프가 커밋 시점에만 부여되기 때문이다. 활성 트랜잭션이 하나라도 있으면 min_timestamp = 0이 되어 조건이 절대 참이 되지 않고, 락이 영원히 회수되지 않는다.

README의 “종료 전에 시간을 충분히 줘야 한다”, “mysqld가 메모리가 안 비워졌다며 에러를 낸다”는 증상이 정확히 이것이다. 저자는 이를 설계의 한계(“victim 하나가 전부 처리해서 느리다”)로 서술했지만, 실제 원인은 두 줄짜리 시점 버그다. 시작 시점에 타임스탬프를 부여하면 대부분 해소된다.

storage/innobase/trx/trx0trx.cc:214–216 · 2015–2017
// 트랜잭션 초기화
#ifdef ITE4065
    trx->timestamp = 0;
#endif

// ... 그리고 커밋 시점에서야 값이 들어간다
#ifdef ITE4065
    trx->timestamp = __sync_fetch_and_add(&trx_sys->timestamp, 1);
#endif
중대

컴파일러가 지워버릴 수 있는 스핀

원자 로드도 volatile도 배리어도 없다. -O2에서 로드가 루프 밖으로 hoist되면 무한 루프다. __sync_* 빌트인은 그 자체로 배리어지만, 그 사이의 평범한 읽기(state, hash, head)는 전혀 보호되지 않는다. 또 trx_sys->mysql_trx_list 순회를 trx_sys->mutex 없이 수행한다(1740행, 7937행).

storage/innobase/lock/lock0lock.cc:3303–3306
//Jiun: If next_lock is logically deleted,
//      connect to next_lock->hash, wait for new transaction
while (next_lock->hash == NULL && cell3333->tail != next_lock);
중대

성능 해석이 낙관적이다 — 64스레드 이하에서는 더 느리다

results/의 원본 sysbench 로그를 직접 집계했다(th* = enhanced, oth* = base, result.png로 교차 확인).

스레드base (qps)enhanced (qps)차이
117,74816,584−7%
899,68493,167−7%
32224,931205,633−9%
128112,208247,272+120%

README는 이 트레이드오프를 한 줄도 언급하지 않고 128스레드 결과만 강조한다. 게다가 개선판은 충돌 검사를 하지 않아 더 적은 일을 하는데도 중간 구간에서 진다. per-lock ut_malloc_nokey와 GC 스핀 오버헤드가 그만큼 크다는 뜻이다. “전역 뮤텍스 경합을 할당 비용과 맞바꿨고, 그 거래는 64스레드를 넘어야 이익” — 이것이 이 데이터의 정직한 결론이며, 그렇게 썼다면 훨씬 강한 보고서가 되었을 것이다.

경미

제출 상태의 위생과 재현성

  • cell3333 같은 디버깅기 변수명이 최종 제출본에 남아 있다.
  • ITE4065_READ_ONLY / ITE4065_READ_WRITEuniv.i에 정의만 되고 어디서도 사용되지 않는다. 게다가 #define X FALSE + #ifdef X 패턴은 항상 참이 되는 함정이다.
  • storage/innobase/include/ite4065.h는 라이선스 헤더만 있는 빈 파일이다.
  • lock0lock.h에서 gclist / gclist_stateDepreciated라고 주석 달아놓고 실제로는 핵심 경로에서 사용 중이다.
  • sysbench 커맨드라인, my.cnf, 테이블 수·크기, 격리 수준이 README에 없다. th*/oth* 파일명 규칙도 설명이 없어 그래프 없이는 어느 쪽이 개선판인지 알 수 없다.
잘한 것

학부 4학년이 InnoDB 락 매니저 내부에 lock-free 리스트와 논리/물리 삭제 분리를 직접 이식했다. perfPolicyMutex 비중이 50%에 달하는 것을 먼저 측정하고, 개선 후 그 항목이 사라진 것을 다시 측정해 제시한 것은 방법론적으로 옳다. 네 프로젝트 중 유일하게 가설 → 측정 → 개선 → 재측정의 사이클을 돌았다.

저장소 차원

중대

MariaDB 전체 트리를 그대로 커밋했다

추적 파일 24,565개 중 24,428개가 project4/mariadb다. fork + 패치나 submodule + patch였다면 실제 기여(수백 줄)가 diff 하나로 드러났을 텐데, 지금은 grep ITE4065를 해야만 보인다. 리뷰 가능성 자체를 잃었고, 포트폴리오로서의 가치도 그만큼 깎인다. Xcode Instruments.trace 204개 파일도 함께 커밋되어 있다.

중대

라이선스 충돌

루트 LICENSE는 MIT인데 트리 안에 GPLv2인 MariaDB 소스가 통째로 들어 있다. GPL 코드를 MIT로 재배포할 수 없다. 공개 GitHub 저장소이므로 실제 문제다. 개별 수정 파일의 GPL 헤더를 보존하고 ite4065.h에 GPL 헤더를 직접 붙인 것은 좋은 감각이었다 — 루트 LICENSE만 어긋난다.

반복되는 패턴

  1. 문서가 코드보다 앞서간다. P1의 “Failure Link ✓”, P2의 “deadlock checking”, P4의 “Milestone 2 ✓” — 셋 다 구현이 주장을 뒷받침하지 못한다. 리뷰어가 README를 믿을 수 없게 되면 나머지 진짜 성과까지 의심받는다.
  2. 인프라에 시간을 쓰고 본론에 덜 썼다. 스레드풀 3종, 인자 파서 2종, 로거, 제네릭 유니크 컨테이너, optional/string_view 폴리필. 그 사이 P2의 데드락 탐지는 return true 한 줄로 죽어 있고 P3의 tableatomic이 아니다. 최신 C++ 표준 활용 자체는 인상적이지만, 표준 기능 과시가 핵심 결함을 가렸다.
  3. 측정 설계가 약하다. 무엇을 세는지(P3: push 수), 무엇과 비교하는지(P4: 의미론이 다른 두 시스템), 정확한지(P2·P3: 검증 결과 없음). 병렬 프로그래밍에서 측정은 부록이 아니라 본문이다.
  4. 자기 인지는 있는데 거기서 멈춘다. README마다 한계를 솔직히 적은 것은 큰 장점이다. 다만 P3의 “왜 그런지 모르겠다”는 perf 한 번이면 풀렸을 문제였다. P4에서 실제로 perf를 쓴 것을 보면 한 학기 안에 이 습관이 생겼다 — 궤적 자체는 좋다.

지금 손본다면

투자 대비 효과 순.

  1. P4 타임스탬프를 커밋 시점 → 시작 시점으로. 2줄. “GC가 안 돈다”는 알려진 결함이 통째로 사라진다.
  2. P2 container.hpp:330return true 삭제. 1줄. 이미 작성해 둔 BFS 사이클 탐지가 살아나고, 과제 요구사항이 비로소 충족된다.
  3. P3 벤치마크 재작성. 스레드별 루프 + 스레드-로컬 카운터. 반나절이면 결과 그래프가 의미를 갖고 README의 물음표가 답으로 바뀐다.
  4. P3 tablestd::atomic<StampedSnap*>으로, moved를 scan 지역 변수로. 정확성이 회복된다.
  5. 루트 README에 “2017년 과제 아카이브 + 알려진 결함 목록” 추가. 포트폴리오 관점에서 가장 효율이 좋다. 결함을 스스로 정확히 아는 엔지니어로 읽히는 것은, 결함이 없는 척하는 것보다 훨씬 강하다.