소리꽃 KeyBloom TIL 20
영상 하나 뽑는 데 30분이 넘게 걸리던 것을 12분까지 줄이며 나온 것들.
느린 것을 고치기
“느리다”는 고칠 수 없다
영상 내보내기가 오래 걸린다는 건 알았지만, 그 상태로는 손을 댈 수가 없었다. 화면을 그리는 게 느린지, 압축이 느린지, 파일로 쓰는 게 느린지 모르는 채로는 어디를 고쳐도 도박이다.
그래서 한 장을 처리하는 동안 각 단계에 얼마씩 쓰는지를 따로 쟀다. 결과는 그리는 데 2, 압축을 기다리는 데 107이었다. 화면 쪽은 죄가 없었다.
앞선 TIL 2에서 「느리다」를 계산과 그리기로 갈랐던 것을, 이번엔 내보내기 전 과정으로 넓힌 셈이다. 성능 이야기가 나오면 제일 먼저 물어야 할 건 “얼마나 느린가”가 아니라 “어느 구간이 느린가”다. 숫자로 갈라 놓기 전에는 어떤 개선안도 근거가 없고, 실제로 여기서도 처음 의심했던 곳은 범인이 아니었다.
“된다”는 답이 “제대로 된다”는 뜻은 아니다
압축(인코딩)은 원래 GPU의 하드웨어 인코더 — 영상 압축 전용 회로 — 가 하게 돼 있었다. 우리가 쓰는 방식이 그 기능을 지원하느냐고 물으면 지원한다고 답했고, 하드웨어를 쓰라고 옵션도 줬다. 그런데 실제 속도는 정확히 소프트웨어로 도는 속도였다.
앱을 감싸는 웹뷰(WebView2)가 그 회로를 못 잡고 조용히 소프트웨어 인코딩으로 내려간 것이다. 어디에도 경고가 없고 “지원함”이라고만 답한다.
기능을 물어보면 대개 “된다/안 된다”만 돌아온다. 얼마나 잘 되는지는 안 알려 준다. 성능이 걸린 기능은 지원 여부 확인으로 끝내지 말고 실제로 재 봐야 한다. 여기서도 재 보지 않았으면 “하드웨어 가속을 쓰고 있다”고 믿은 채 다른 데를 파고 있었을 것이다.
경로를 바꿀 것인가 양을 줄일 것인가
압축을 앱에 번들한 ffmpeg에 맡기니(GPU 벤더별 하드웨어 인코더를 직접 부를 수 있다) 그 단계는 25배 빨라졌다. 그러자 이번엔 화면에서 그 도구로 그림을 넘기는 통로가 병목이 됐다.
통로가 느린 줄 알고 다른 통로를 실제로 만들어 재 봤다. 거의 같았다. 특정 통로가 느린 게 아니라 그 경계를 넘어 큰 데이터를 옮기는 것 자체가 느렸다.
옮기는 것이 느릴 때 손댈 곳은 둘이다. 더 빠른 길을 찾거나, 옮길 양을 줄이거나. 길을 바꾸는 쪽이 코드를 덜 고칠 것 같아 먼저 손이 가는데, 경계 자체가 느린 경우에는 어떤 길로 가도 똑같다. 여기서는 그림을 압축해서 넘기니 40분의 1로 줄었고 그걸로 끝났다.
새 통로를 만들어 재 본 게 낭비처럼 보이지만 아니었다. 그 측정이 없었으면 “다른 통로가 더 빨랐을지도 모른다”는 미련이 계속 남았을 것이다.
병렬의 한계와 비용
나눌 수 없는 구간이 천장을 정한다
압축을 워커(브라우저에서 백그라운드로 도는 별도 스레드) 여러 개로 나눠 동시에 돌리니 크게 빨라졌다. 그런데 어느 지점부터는 갈래를 더 늘려도 총 시간이 안 줄었다.
남아 있는 구간 때문이다. 화면을 그리고 그 그림을 읽어 내는 일은 한 줄로만 할 수 있어서 나눌 수가 없다. 나머지를 아무리 잘게 나눠도 총 시간은 그 구간 아래로 안 내려간다. 병렬로 못 나누는 부분이 전체 속도의 천장을 정한다는 이 이야기를 암달의 법칙이라고 한다.
그래서 갈래 수를 적당한 선에서 고정했다. 더 늘리면 메모리만 먹고 빨라지지는 않는다.
이건 사람 붙이는 일에도 똑같이 적용된다. 일을 나눠 맡길 수 없는 구간이 남아 있으면, 사람을 두 배로 붙여도 두 배로 빨라지지 않는다. 속도를 더 원하면 인원이 아니라 그 나눌 수 없는 구간을 봐야 한다.
병렬은 순서를 흩뜨린다
여러 갈래로 나눠 처리하면 끝나는 순서가 뒤섞인다. 그런데 영상은 장면 순서가 틀리면 안 된다. 그래서 먼저 끝난 것을 붙들고 앞 순서가 올 때까지 기다렸다가 차례대로 내보내야 했다.
앞서 나가는 것도 막아야 했다. 처리가 못 따라오는데 계속 밀어 넣으면 대기 줄이 메모리를 먹는다. 일정 수준 이상 밀리면 잠시 멈추게 했는데, 이렇게 밀어 넣는 쪽 속도를 뒤에서 조절하는 걸 백프레셔라고 한다.
병렬로 바꾸면 “재정렬”과 “제동”이 세트로 따라온다. 속도만 보고 나누면 이 둘을 나중에 버그로 만난다.
품질과 표시
중간 품질은 최종 품질을 넘을 이유가 없다
중간 단계에서 그림을 압축해 넘기는데, 압축은 화질을 조금 깎는다. 처음엔 중간 단계니까 손실이 없어야 한다고 생각했다.
중간 압축은 JPEG로 했는데, 최종 결과물인 H.264(가장 널리 쓰는 영상 압축 형식)도 어차피 손실 압축이다. 최종에서 깎일 것보다 중간에서 덜 깎으면 그 차이는 결과물에 남지 않는다. 중간을 무손실로 지키는 건 비용만 내고 얻는 게 없다.
품질 기준은 항상 마지막 단계에서 역산한다. 중간에 필요 이상으로 좋은 품질을 유지하는 건 눈에 안 보이는 낭비다.
표시를 켜는 것과 표시가 보이는 것은 다르다
저장 중이라는 표시를 켠 다음 바로 무거운 작업을 시작했더니, 표시가 작업이 끝날 때쯤에야 나타났다. 표시를 켜라고 지시해도 화면이 실제로 다시 그려질 틈이 없으면 사용자 눈에는 안 뜬 것이다.
그래서 표시를 켠 뒤 화면이 한 번 그려지는 걸 확인하고 나서 작업을 시작하게 했다.
“표시가 안 뜬다”는 제보가 오면 표시 코드보다 그 뒤에 이어지는 작업을 먼저 봐야 한다.
초반 예상 시간은 거짓말이다
남은 시간을 진행률로 역산해 보여줬는데, 시작하자마자는 30분 넘게 남았다고 떴다가 금방 확 줄었다. 처음 준비하는 시간이 진행률이 작을 때 그대로 증폭돼서 그렇다.
숫자를 보정하는 대신, 어느 정도 진행되기 전에는 “계산 중”으로 두기로 했다. 틀린 숫자를 보여 주는 것보다 아직 모른다고 하는 게 낫다. 30분이라고 떴다가 3분에 끝나면 사용자는 그 표시를 앞으로도 안 믿는다.
추정치를 화면에 올릴 때는 “언제부터 믿을 만한가”를 같이 정해야 한다.
요약
- “느리다”는 고칠 수 없다. 구간별로 갈라 재기 전에는 어떤 개선안도 근거가 없다
- “지원한다”는 답은 성능을 보장하지 않는다 — 성능이 걸렸으면 실제로 재 본다
- 옮기는 게 느릴 땐 길을 바꿀지 양을 줄일지를 가른다. 경계 자체가 느리면 어느 길이든 같다
- 나눌 수 없는 구간이 남으면 갈래를 늘려도 그 아래로 안 내려간다 — 사람도 마찬가지다
- 병렬로 바꾸면 재정렬과 제동이 세트로 따라온다
- 품질 기준은 마지막 단계에서 역산한다. 중간을 과하게 지키는 건 안 보이는 낭비다
- 표시를 켠 직후 무거운 일을 이으면 표시가 안 뜬 것처럼 보인다
- 초반 예상 시간은 틀린다. 틀린 숫자보다 “계산 중”이 낫다