노트

CPU 바운드와 I/O 바운드

CPU-bound vs I/O-bound

CS#js#performance · 연결된 개념 4개

쉽게 말하면

김치를 담글 때 배추를 써는 손이 느려서 늦어지면 CPU 바운드, 배추가 절여지기를 기다려서 늦어지면 I/O 바운드예요. 무엇 때문에 늦는지에 따라 손을 늘릴지, 기다리는 동안 딴 일을 할지가 갈려요.

비유의 한계 써는 일을 옆 사람에게 넘긴다고 전체가 빨라지지는 않아요. Web Worker로 옮긴 계산은 그대로 오래 걸리고, 메인 스레드가 화면 반응을 지킬 수 있게 될 뿐이에요. 빨라지려면 일을 쪼개 여러 워커에 나눠야 해요.

작업 시간이 무엇에 막히는지(bound)로 작업을 나누는 말이다. CPU 바운드(CPU-bound)는 완료 시간이 주로 프로세서의 계산 속도로 정해지는 작업이고, I/O 바운드(I/O-bound)는 네트워크·디스크·DB 같은 입출력(Input/Output)을 기다리는 시간으로 정해지는 작업이다.

CPU 바운드I/O 바운드
시간이 드는 곳이미 가진 데이터를 계산응답이 오기를 기다림
예정렬, 이미지 처리, 해시·암호화, 압축, 큰 JSON 파싱fetch, 파일 읽기, DB 쿼리
기다리는 동안 CPU바쁘다대부분 쉰다
맞는 대응다른 스레드·프로세스로 보내고, 쪼개서 병렬화비동기 I/O로 기다리는 동안 다른 일, 동시 요청, 캐시

병목에 맞지 않는 처방은 효과가 없다. fetch를 Worker로 옮겨도 네트워크는 빨라지지 않는다. 반대로 무거운 계산을 async 함수에 넣어도 같은 스레드에서 돌기 때문에 이벤트 루프를 붙잡는다(동기·비동기와 블로킹·논블로킹, 이벤트 루프).

브라우저에서 CPU 바운드 작업 옮기기

메인 스레드에서 긴 계산을 돌리면 입력 처리와 렌더링이 멈춰 INP가 나빠진다. Web Worker는 스크립트를 백그라운드 스레드에서 돌려 이 문제를 피한다.

// main.js
const worker = new Worker("/worker.js");
worker.onmessage = (event) => render(event.data);
// postMessage(message, transfer): transfer에 넣은 ArrayBuffer는 복사 없이 넘어간다
worker.postMessage(pixels.buffer, [pixels.buffer]);
 
// worker.js
self.onmessage = (event) => {
  const result = heavyFilter(new Uint8Array(event.data));
  self.postMessage(result);
};
  • Worker는 페이지의 DOM을 직접 건드릴 수 없다. 결과를 메시지로 돌려보내고 화면은 메인 스레드가 바꾼다
  • postMessage로 보낸 데이터는 기본적으로 구조화된 복제(structured clone)로 복사된다. 큰 데이터는 전송 가능 객체(transferable object)로 넘기면 복사하지 않는 대신, 보낸 쪽의 ArrayBuffer는 분리(detached)되어 더 쓸 수 없다
  • Worker 하나로 옮긴 계산은 같은 시간이 걸린다. 얻는 것은 메인 스레드의 응답성이다. 전체 시간을 줄이려면 알고리즘이 허락하는 한 작업을 쪼개 여러 Worker에 나눈다
  • Worker 생성과 메시지 복사에도 비용이 있어 아주 작은 계산은 오히려 느려질 수 있다

서버에서도 같은 기준을 쓴다. Node.js 문서는 CPU 바운드 작업을 두 가지로 다룬다. 계산을 잘게 나눠 이벤트 루프에 차례를 양보하는 분할(partitioning)과, 워커 풀·자식 프로세스로 넘기는 위임(offloading)이다. 위임은 스레드·프로세스 사이에 데이터를 직렬화하는 통신 비용이 든다. I/O 대기가 많은 서버는 이벤트 루프나 가상 스레드로 동시 요청을 받는다.

출처: MDN — Using Web Workers · MDN — Transferable objects · Node.js — Don't Block the Event Loop

참고: Wikipedia — CPU-bound · Wikipedia — I/O bound

연결된 개념

이 노트를 가리키는 문서

뜻이 가까운 노트

  • 디바운스

    디바운스(Debounce)는 짧은 간격으로 이어지는 호출을 한 묶음으로 보고, 호출이 멈춘 뒤 정해진 시간이 지나면 한 번만 실행하는 기법이다. 입력처럼 잦게 일어나는 이벤트에 무거운 작업(결과 필터링, 추천어 요청)이 붙을 때 쓴다.

  • 롱 태스크

    메인 스레드를 50ms 넘게 붙잡는 작업. W3C Long Tasks 명세는 이벤트 루프 태스크(뒤따르는 마이크로태스크 처리 포함), 렌더링 갱신 단계, 이벤트 루프 밖에서 브라우저가 UI 스레드로 하는 일 가운데 50ms를 넘는 것을 롱 태스크로 본다. 태스크는 도중에 끊기지 않으므로 그동안 클릭·키 입력 처리와 화면 갱신이 모두 밀린다(이벤트 루프).

  • 이미지 loading 속성

    ==img·iframe의 loading 속성으로 리소스를 언제 내려받을지 정한다. eager(기본)는 태그를 만나자마자, lazy는 화면에 가까워질 때까지 미룬다.== JS(JavaScript) 없이 동작하는 브라우저 기본 기능이라 IntersectionObserver로 직접 만들 필요가 없다.

  • 업스트림과 다운스트림

    흐름의 방향을 강물에 빗댄 말이다. 다만 무엇의 흐름을 기준으로 하느냐에 따라 방향이 반대로 쓰인다. 서비스 간 호출을 말할 때는 요청이 흘러가는 쪽을 기준으로 다운스트림 = 내가 호출하는 쪽, 업스트림 = 나를 호출하는 쪽이라 부르는 경우가 많다. 아래 설명은 이 용법을 따른다.

  • WebGPU

    웹페이지에서 GPU로 그래픽과 범용 계산을 하는 브라우저 API. WebGL의 후속으로, 브라우저 안에서 ML 모델을 돌리는 기반이 된다.

보기 옵션