이 글은 Today I Learn 시리즈의 80번째 기록입니다. (총 125개)

영상 하나 뽑는 데 30분이 넘게 걸리던 것을 12분까지 줄이며 나온 것들.

느린 것을 고치기

“느리다”는 고칠 수 없다

영상 내보내기가 오래 걸린다는 건 알았지만, 그 상태로는 손을 댈 수가 없었다. 화면을 그리는 게 느린지, 압축이 느린지, 파일로 쓰는 게 느린지 모르는 채로는 어디를 고쳐도 도박이다.

그래서 한 장을 처리하는 동안 각 단계에 얼마씩 쓰는지를 따로 쟀다. 결과는 그리는 데 2, 압축을 기다리는 데 107이었다. 화면 쪽은 죄가 없었다.

앞선 TIL 2에서 「느리다」를 계산과 그리기로 갈랐던 것을, 이번엔 내보내기 전 과정으로 넓힌 셈이다. 성능 이야기가 나오면 제일 먼저 물어야 할 건 “얼마나 느린가”가 아니라 “어느 구간이 느린가”다. 숫자로 갈라 놓기 전에는 어떤 개선안도 근거가 없고, 실제로 여기서도 처음 의심했던 곳은 범인이 아니었다.

“된다”는 답이 “제대로 된다”는 뜻은 아니다

압축(인코딩)은 원래 GPU의 하드웨어 인코더 — 영상 압축 전용 회로 — 가 하게 돼 있었다. 우리가 쓰는 방식이 그 기능을 지원하느냐고 물으면 지원한다고 답했고, 하드웨어를 쓰라고 옵션도 줬다. 그런데 실제 속도는 정확히 소프트웨어로 도는 속도였다.

앱을 감싸는 웹뷰(WebView2)가 그 회로를 못 잡고 조용히 소프트웨어 인코딩으로 내려간 것이다. 어디에도 경고가 없고 “지원함”이라고만 답한다.

기능을 물어보면 대개 “된다/안 된다”만 돌아온다. 얼마나 잘 되는지는 안 알려 준다. 성능이 걸린 기능은 지원 여부 확인으로 끝내지 말고 실제로 재 봐야 한다. 여기서도 재 보지 않았으면 “하드웨어 가속을 쓰고 있다”고 믿은 채 다른 데를 파고 있었을 것이다.

경로를 바꿀 것인가 양을 줄일 것인가

압축을 앱에 번들한 ffmpeg에 맡기니(GPU 벤더별 하드웨어 인코더를 직접 부를 수 있다) 그 단계는 25배 빨라졌다. 그러자 이번엔 화면에서 그 도구로 그림을 넘기는 통로가 병목이 됐다.

통로가 느린 줄 알고 다른 통로를 실제로 만들어 재 봤다. 거의 같았다. 특정 통로가 느린 게 아니라 그 경계를 넘어 큰 데이터를 옮기는 것 자체가 느렸다.

옮기는 것이 느릴 때 손댈 곳은 둘이다. 더 빠른 길을 찾거나, 옮길 양을 줄이거나. 길을 바꾸는 쪽이 코드를 덜 고칠 것 같아 먼저 손이 가는데, 경계 자체가 느린 경우에는 어떤 길로 가도 똑같다. 여기서는 그림을 압축해서 넘기니 40분의 1로 줄었고 그걸로 끝났다.

새 통로를 만들어 재 본 게 낭비처럼 보이지만 아니었다. 그 측정이 없었으면 “다른 통로가 더 빨랐을지도 모른다”는 미련이 계속 남았을 것이다.


병렬의 한계와 비용

나눌 수 없는 구간이 천장을 정한다

압축을 워커(브라우저에서 백그라운드로 도는 별도 스레드) 여러 개로 나눠 동시에 돌리니 크게 빨라졌다. 그런데 어느 지점부터는 갈래를 더 늘려도 총 시간이 안 줄었다.

남아 있는 구간 때문이다. 화면을 그리고 그 그림을 읽어 내는 일은 한 줄로만 할 수 있어서 나눌 수가 없다. 나머지를 아무리 잘게 나눠도 총 시간은 그 구간 아래로 안 내려간다. 병렬로 못 나누는 부분이 전체 속도의 천장을 정한다는 이 이야기를 암달의 법칙이라고 한다.

그래서 갈래 수를 적당한 선에서 고정했다. 더 늘리면 메모리만 먹고 빨라지지는 않는다.

이건 사람 붙이는 일에도 똑같이 적용된다. 일을 나눠 맡길 수 없는 구간이 남아 있으면, 사람을 두 배로 붙여도 두 배로 빨라지지 않는다. 속도를 더 원하면 인원이 아니라 그 나눌 수 없는 구간을 봐야 한다.

병렬은 순서를 흩뜨린다

여러 갈래로 나눠 처리하면 끝나는 순서가 뒤섞인다. 그런데 영상은 장면 순서가 틀리면 안 된다. 그래서 먼저 끝난 것을 붙들고 앞 순서가 올 때까지 기다렸다가 차례대로 내보내야 했다.

앞서 나가는 것도 막아야 했다. 처리가 못 따라오는데 계속 밀어 넣으면 대기 줄이 메모리를 먹는다. 일정 수준 이상 밀리면 잠시 멈추게 했는데, 이렇게 밀어 넣는 쪽 속도를 뒤에서 조절하는 걸 백프레셔라고 한다.

병렬로 바꾸면 “재정렬”과 “제동”이 세트로 따라온다. 속도만 보고 나누면 이 둘을 나중에 버그로 만난다.


품질과 표시

중간 품질은 최종 품질을 넘을 이유가 없다

중간 단계에서 그림을 압축해 넘기는데, 압축은 화질을 조금 깎는다. 처음엔 중간 단계니까 손실이 없어야 한다고 생각했다.

중간 압축은 JPEG로 했는데, 최종 결과물인 H.264(가장 널리 쓰는 영상 압축 형식)도 어차피 손실 압축이다. 최종에서 깎일 것보다 중간에서 덜 깎으면 그 차이는 결과물에 남지 않는다. 중간을 무손실로 지키는 건 비용만 내고 얻는 게 없다.

품질 기준은 항상 마지막 단계에서 역산한다. 중간에 필요 이상으로 좋은 품질을 유지하는 건 눈에 안 보이는 낭비다.

표시를 켜는 것과 표시가 보이는 것은 다르다

저장 중이라는 표시를 켠 다음 바로 무거운 작업을 시작했더니, 표시가 작업이 끝날 때쯤에야 나타났다. 표시를 켜라고 지시해도 화면이 실제로 다시 그려질 틈이 없으면 사용자 눈에는 안 뜬 것이다.

그래서 표시를 켠 뒤 화면이 한 번 그려지는 걸 확인하고 나서 작업을 시작하게 했다.

“표시가 안 뜬다”는 제보가 오면 표시 코드보다 그 뒤에 이어지는 작업을 먼저 봐야 한다.

초반 예상 시간은 거짓말이다

남은 시간을 진행률로 역산해 보여줬는데, 시작하자마자는 30분 넘게 남았다고 떴다가 금방 확 줄었다. 처음 준비하는 시간이 진행률이 작을 때 그대로 증폭돼서 그렇다.

숫자를 보정하는 대신, 어느 정도 진행되기 전에는 “계산 중”으로 두기로 했다. 틀린 숫자를 보여 주는 것보다 아직 모른다고 하는 게 낫다. 30분이라고 떴다가 3분에 끝나면 사용자는 그 표시를 앞으로도 안 믿는다.

추정치를 화면에 올릴 때는 “언제부터 믿을 만한가”를 같이 정해야 한다.


요약

  • “느리다”는 고칠 수 없다. 구간별로 갈라 재기 전에는 어떤 개선안도 근거가 없다
  • “지원한다”는 답은 성능을 보장하지 않는다 — 성능이 걸렸으면 실제로 재 본다
  • 옮기는 게 느릴 땐 길을 바꿀지 양을 줄일지를 가른다. 경계 자체가 느리면 어느 길이든 같다
  • 나눌 수 없는 구간이 남으면 갈래를 늘려도 그 아래로 안 내려간다 — 사람도 마찬가지다
  • 병렬로 바꾸면 재정렬과 제동이 세트로 따라온다
  • 품질 기준은 마지막 단계에서 역산한다. 중간을 과하게 지키는 건 안 보이는 낭비다
  • 표시를 켠 직후 무거운 일을 이으면 표시가 안 뜬 것처럼 보인다
  • 초반 예상 시간은 틀린다. 틀린 숫자보다 “계산 중”이 낫다

Series: Today I Learn

1 C++ 자료형(Data Type) 2 MD5 vs pHash 3 C++에서 함수의 선언과 정의 4 Tkinter padx, pady 5 메모리와 포인터 변수 6 Call by Value, Call by Reference, Call by Pointer 비교 7 const 8 Gemfile — Jekyll 프로젝트의 의존성 파일 9 kramdown-parser-gfm — Jekyll의 GFM 파서 10 파서(Parser) 11 AHU vs OHU 12 I might try it vs I'll try it 뉘앙스 차이 13 SESSION_EXPIRE_AT_BROWSER_CLOSE=True 14 configuration key 15 Git stash vs discard 16 subprocess.Popen으로 Windows 탐색기에 명령어를 전달 17 Post 잔디 분석하기 18 Google Sheets Sync 최적화 19 DSL (Domain Specific Language)과 GPL (General Purpose Language) 20 마크다운 표 그리는 방법 21 쿼리 파라미터(Query Parameter). 기존 QR코드 재활용 22 Django 보안 취약점 점검 및 수정 23 OOP Object-Oriented Programming 객체 지향 프로그래밍 24 Fernet 대칭 암호화 25 Jekyll 코드블록 안의 Liquid 태그 26 insertOnConflictUpdate vs DoUpdate(target) 27 세션 필터 28 아코디언(Accordiaon) UI를 펼친상태로 만들기 29 input의 step 30 Word Cloud 31 Google Sheets를 데이터 버스로(with AppSheet) 32 Django 모델 텍스트 필드 자동 수집 패턴 33 localStorage로 섹션 토글 상태 유지 34 순차 ID 생성(`select_for_update()` + `max()` 조합) 35 역참조 검색과 distinct() 36 xlsx 다운로드와 로딩 오버레이 충돌 37 Android 파일 공유 MIME 타입 38 AssetManifest — Flutter 빌드 타임 asset 목록 런타임 조회 39 UTF-8 BOM과 PowerShell 파일 쓰기 40 소리꽃 KeyBloom TIL 1 41 소리꽃 KeyBloom TIL 2 42 소리꽃 KeyBloom TIL 3 43 메트로놈 Simple Metronome TIL 1 44 소리꽃 KeyBloom TIL 4 45 메트로놈 Simple Metronome TIL 2 46 소리꽃 KeyBloom TIL 5 47 메트로놈 Simple Metronome TIL 3 48 정적 블로그 SEO 정비와 Pagefind 검색 도입 49 메트로놈 Simple Metronome TIL 4 50 안드로이드 AudioTrack 연속 재생, 실측 피커 정렬, 카메라 토치 플래시 51 메트로놈 Simple Metronome TIL 5 52 모래게임 Sandrop TIL 1 53 메트로놈 Simple Metronome TIL 6 54 모래게임 Sandrop TIL 2 55 모래게임 Sandrop TIL 3 56 메트로놈 Simple Metronome TIL 7 57 모래게임 Sandrop TIL 4 58 모래게임 Sandrop TIL 5 59 모래게임 Sandrop TIL 6 60 소리꽃 KeyBloom TIL 6 61 모래게임 Sandrop TIL 7 62 모래게임 Sandrop TIL 8 63 소리꽃 KeyBloom TIL 7 64 모래게임 Sandrop TIL 9 65 소리꽃 KeyBloom TIL 8 66 모래게임 Sandrop TIL 10 67 소리꽃 KeyBloom TIL 9 68 소리꽃 KeyBloom TIL 10 69 소리꽃 KeyBloom TIL 11 70 소리꽃 KeyBloom TIL 12 71 소리꽃 KeyBloom TIL 13 72 온실 GreenHouse TIL 1 73 소리꽃 KeyBloom TIL 14 74 온실 GreenHouse TIL 2 75 소리꽃 KeyBloom TIL 15 76 소리꽃 KeyBloom TIL 16 77 소리꽃 KeyBloom TIL 17 78 소리꽃 KeyBloom TIL 18 79 소리꽃 KeyBloom TIL 19 80 소리꽃 KeyBloom TIL 20 읽는 중 81 로그스톤 상표 셀프 출원 TIL 1 82 소리꽃 KeyBloom TIL 21 83 소리꽃 KeyBloom TIL 22 84 소리꽃 KeyBloom TIL 23 85 소리꽃 KeyBloom TIL 24 86 MiniMacro TIL 1 87 Astro가 무엇인지, 왜 옮기는지 88 MiniMacro TIL 2 89 메트로놈 Simple Metronome TIL 8 90 메트로놈 Simple Metronome TIL 9 91 블로그 Astro 이관 TIL 92 오픈데이 Openday TIL 1 93 오픈데이 Openday TIL 2 94 Unreal Engine MCP TIL 1 95 콘티온 Conti On TIL 11 96 오픈데이 Openday TIL 3 97 오픈데이 Openday TIL 4 98 오픈데이 Openday TIL 5 99 ScorePlayer TIL 1 100 BuildingHub TIL 1 101 ScorePlayer TIL 2 102 BuildingHub TIL 2 103 ScorePlayer TIL 3 104 Unreal Engine MCP TIL 2 105 궁 미로 PalaceMaze TIL 1 106 메트로놈 Simple Metronome TIL 10 107 메트로놈 Simple Metronome TIL 11 108 오픈데이 Openday TIL 6 109 BuildingHub TIL 3 110 Obsidian vault에서 코드만 빼기 — directory junction 111 ScorePlayer TIL 4 112 오픈데이 Openday TIL 7 113 URL은 바뀔 수 있는 것에 묶지 않는다 114 도구는 기능이 아니라 내 일에 맞는지로 고른다 115 돌이키기 힘든 변경은 작게 먼저 확인한다 116 궁 미로 PalaceMaze TIL 3 117 궁 미로 PalaceMaze TIL 2 118 소리꽃 KeyBloom TIL 29 119 오픈데이 Openday TIL 8 120 BuildingHub TIL 4 121 ScorePlayer TIL 5 122 BuildingHub TIL 5 123 MiniMacro TIL 3 124 소리꽃 KeyBloom TIL 30 125 궁 미로 PalaceMaze TIL 4