앱의 자바스크립트에서 핫 경로를 WebAssembly로 대체

계속 빠릅니다.

이전에서 기사: WebAssembly가 를 사용하면 C/C++의 라이브러리 생태계를 웹에 가져올 수 있습니다. 하나의 앱은 squoosh인데, 이는 C/C++ 라이브러리를 폭넓게 사용하는 것입니다. 웹 앱을 통해 기존에 사용하던 다양한 코덱으로 이미지를 C++에서 WebAssembly로 컴파일합니다.

WebAssembly는 저장된 바이트코드를 실행하는 저수준 가상 머신으로, (.wasm 파일) 이 바이트 코드는 다른 컴퓨터에 비해 훨씬 빠르게 컴파일되고 호스트 시스템에 최적화될 수 있다는 것을 JavaScript는 가능합니다. WebAssembly는 많은 시간이 소요되는 작업 코드를 실행할 수 있는 환경을 샌드박싱과 임베딩을 염두에 두고 있습니다.

제가 경험한 바에 따르면 웹에서 발생하는 성능 문제는 대부분 하지만 가끔 앱에서 많은 시간이 걸리고 계산 비용이 많이 드는 태스크입니다. WebAssembly 활용 여기에서 확인하세요.

핫 패스

Google은 이러한 문제를 해결하기 위해 JavaScript 함수를 작성했습니다. 이 함수는 이미지 버퍼를 90도의 배수만큼 회전합니다. 동안 OffscreenCanvas는 다음과 같은 경우에 적합합니다. 타겟팅한 브라우저 전반에서 지원되지 않습니다. 버그가 있는지 확인합니다.

이 함수는 입력 이미지의 모든 픽셀을 반복하여 입력 이미지의 회전을 달성하기 위해 출력 이미지에서 다른 위치를 지정합니다. 4094pxx 4096px 이미지 (16 메가픽셀)라면 내부 코드 블록에 '핫 경로'라고 합니다. 그럼에도 불구하고 반복 횟수로 테스트한 브라우저 3개 중 2개가 2번 만에 작업을 마쳤습니다. 초 이하입니다. 이 유형의 상호작용에 허용되는 기간입니다.

for (let d2 = d2Start; d2 >= 0 && d2 < d2Limit; d2 += d2Advance) {
    for (let d1 = d1Start; d1 >= 0 && d1 < d1Limit; d1 += d1Advance) {
    const in_idx = ((d1 * d1Multiplier) + (d2 * d2Multiplier));
    outBuffer[i] = inBuffer[in_idx];
    i += 1;
    }
}

하지만 브라우저 하나를 사용하면 8초 이상 걸립니다. 브라우저가 자바스크립트를 최적화하는 방식 매우 복잡하며, 엔진마다 최적화가 다릅니다. 일부는 원시 실행에 맞게 최적화하고 일부는 DOM과의 상호작용에 맞게 최적화합니다. 포함 이 경우 한 브라우저에서 최적화되지 않은 경로에 도달했습니다.

반면에 WebAssembly는 원시 실행 속도를 기반으로 빌드됩니다. 그래서 이와 같은 코드에 여러 브라우저에서 빠르고 예측 가능한 성능을 원한다면 WebAssembly가 도움이 될 수 있습니다.

예측 가능한 성능을 위한 WebAssembly

일반적으로 JavaScript와 WebAssembly는 동일한 최대 성능을 달성할 수 있습니다. 그러나 JavaScript의 경우 이 성능은 '빠른 경로'를 통해서만 도달할 수 있습니다. 이러한 '빠른 경로'를 따르는 것이 쉽지 않을 때가 많습니다. 한 가지 주요 이점은 WebAssembly는 여러 브라우저에서도 예측 가능한 성능을 제공합니다. 엄격한 타이핑 및 하위 수준 아키텍처를 사용하면 컴파일러가 더 강력하고 사용하면 WebAssembly 코드가 한 번만 최적화되고 항상 '빠른 경로'를 사용합니다.

WebAssembly용으로 작성

이전에는 C/C++ 라이브러리를 가져와 WebAssembly로 컴파일하여 소개해 드리려고 합니다. 라이브러리의 코드는 실제로 건드리지 않았습니다. 방금 소량의 C/C++ 코드를 작성하여 브라우저 간의 브리지를 형성했습니다. 볼 수 있습니다 이번에는 동기가 다릅니다. 처음부터 WebAssembly를 염두에 두고 개발해야 했습니다. 장점이 있습니다.

WebAssembly 아키텍처

WebAssembly 문서를 작성할 때는 실제로 WebAssembly가 무엇인지를 알 수 있습니다.

WebAssembly.org를 인용하려면 다음과 같이 하세요.

C 또는 Rust 코드를 WebAssembly로 컴파일하면 .wasm이 발생합니다. 이 파일을 만듭니다. 이 선언은 "가져오기" 해당 모듈이 환경에서 예상되는 내보내기 목록을 모듈은 호스트 (함수, 상수, 메모리 청크)에 제공하고 물론 안에 포함된 함수에 대한 실제 이진 명령입니다.

이 문제를 조사해 보기 전까지 깨닫지 못한 것이 WebAssembly: '스택 기반 가상 머신' 데이터가 청크에 저장되지 않은 경우 메모리와 어셈블리에 포함되어 있습니다 스택은 완전히 VM 내부이며 웹 개발자가 액세스할 수 없습니다 (DevTools를 통하는 경우 제외). 따라서 추가 메모리가 전혀 필요하지 않은 WebAssembly 모듈을 작성하고 VM 내부 스택만 사용합니다

여기서는 임의의 액세스를 허용하기 위해 추가 메모리를 사용해야 합니다. 이미지의 픽셀에 매핑하고 이미지의 회전된 버전을 생성합니다. 이것은 WebAssembly.Memory의 용도는 무엇인가요?

메모리 관리

일반적으로 추가 메모리를 사용하면 어떤 식으로든 관리할 수 있습니다. 메모리의 어느 부분이 사용됩니까? 어떤 것이 무료인가요? 예를 들어 C에는 메모리 공간을 찾는 malloc(n) 함수가 있습니다. 총 n바이트 중 하나입니다. 이러한 종류의 함수를 '할당자'라고도 합니다. 물론 사용 중인 할당자의 구현은 WebAssembly 모듈을 빌드하여 파일 크기를 늘립니다. 이 크기 및 성능 이러한 메모리 관리 기능의 종류에 따라 알고리즘이 사용되므로 많은 언어가 여러 가지 구현을 제공합니다. ('dmalloc', 'emmalloc', 'wee_alloc' 등) 중에서 선택할 수 있습니다.

여기서는 입력 이미지의 크기를 알고 있으므로 (따라서 크기)을 미리 살펴봤습니다. 여기서는 기존에는 입력 이미지의 RGBA 버퍼를 매개변수를 WebAssembly 함수에 추가하고 회전된 이미지를 반환으로 반환합니다. 값으로 사용됩니다. 이 반환 값을 생성하려면 할당자를 활용해야 합니다. 하지만 필요한 총 메모리 양 (입력 크기의 두 배)을 알고 있기 때문에 한 번은 입력, 다른 한 번은 출력)에 입력 이미지를 JavaScript를 사용하는 WebAssembly 메모리를 사용하려면 WebAssembly 모듈을 실행하여 두 번째는 이미지를 회전한 다음 JavaScript를 사용하여 결과를 다시 읽습니다. 우리가 얻을 수 있는 메모리 관리를 전혀 사용하지 않아도 됩니다.

선택의 즐거움 제공

원래 JavaScript 함수를 WebAssembly-fy를 사용하면 그것이 순전히 전산적이고 코드를 실행할 수 없습니다. 따라서 직선적이어야 합니다. 이 코드를 원하는 언어로 포팅할 수 있습니다. 3가지 언어를 평가했습니다. (C/C++, Rust, AssemblyScript 등) WebAssembly로 컴파일됩니다. 유일한 질문 각 언어에 대해 답해야 합니다. '원시 메모리에 액세스하는 방법' 어떻게 해야 할까요?

C 및 Emscripten

Emscripten은 WebAssembly 타겟용 C 컴파일러입니다. Emscripten의 목표는 GCC 또는 clang과 같은 잘 알려진 C 컴파일러의 삽입형 대체 함수로서의 함수 대부분 플래그와 호환됩니다. 이는 Emscripten의 임무에서 핵심적인 부분입니다. 기존 C 및 C++ 코드를 WebAssembly로 컴파일하는 작업을 있습니다.

원시 메모리에 액세스하는 것은 C의 본질적으로 그 자체를 위한 포인터가 존재합니다. 이유:

uint8_t* ptr = (uint8_t*)0x124;
ptr[0] = 0xFF;

여기서는 숫자 0x124를 부호 없는 8비트 포인터로 바꿉니다. 정수 (또는 바이트)입니다. 이렇게 하면 ptr 변수를 배열로 효과적으로 변환할 수 있습니다. 다른 배열과 마찬가지로 사용할 수 있는 메모리 주소 0x124에서 시작합니다. 읽기 및 쓰기를 위한 개별 바이트에 액세스할 수 있습니다. 이 경우에는 재정렬하려는 이미지의 RGBA 버퍼를 보는 경우 있습니다. 픽셀을 이동하려면 실제로 한 번에 4바이트를 연속으로 이동해야 합니다. (각 채널에 대해 1바이트: R, G, B, A). 이를 더 쉽게 하기 위해 부호 없는 32비트 정수 배열입니다. 규칙에 따라 입력 이미지는 입력 이미지 바로 다음에 출력 이미지가 시작됩니다. 종료:

int bpp = 4;
int imageSize = inputWidth * inputHeight * bpp;
uint32_t* inBuffer = (uint32_t*) 4;
uint32_t* outBuffer = (uint32_t*) (inBuffer + imageSize);

for (int d2 = d2Start; d2 >= 0 && d2 < d2Limit; d2 += d2Advance) {
    for (int d1 = d1Start; d1 >= 0 && d1 < d1Limit; d1 += d1Advance) {
    int in_idx = ((d1 * d1Multiplier) + (d2 * d2Multiplier));
    outBuffer[i] = inBuffer[in_idx];
    i += 1;
    }
}
드림

전체 자바스크립트 함수를 C로 포팅한 후에는 C 파일을 컴파일할 수 있습니다. emcc:

$ emcc -O3 -s ALLOW_MEMORY_GROWTH=1 -o c.js rotate.c

항상 그렇듯이 emscripten은 c.js라는 글루 코드 파일과 wasm 모듈을 생성합니다. c.wasm입니다. wasm 모듈은 gzip을 최대 260바이트로 하지만 글루 코드는 gzip 이후에 약 3.5KB입니다. 약간의 실수를 한 후에 글루 코드를 작성하고 Vanilla API로 WebAssembly 모듈을 인스턴스화합니다. 아무것도 사용하지 않는 한 Emscripten을 사용하면 종종 가능합니다. 삭제할 수 있습니다.

Rust

Rust는 런타임이 없는 풍부한 유형 시스템을 갖춘 새로운 최신 프로그래밍 언어입니다. 메모리 안전과 스레드 안전을 보장하는 소유권 모델이 있습니다. 러스트 또한 핵심 기능으로 WebAssembly를 지원하며 Rust팀은 은 WebAssembly 생태계에 훌륭한 도구를 많이 제공했습니다.

이러한 도구 중 하나는 개발자님의 wasm-pack입니다. rustWasm 실무 그룹의 도움을 받았습니다. wasm-pack 코드를 가져와서 작동하는 웹 친화적 모듈로 웹팩과 같은 번들러를 사용하면 즉시 사용할 수 있습니다. wasm-pack은(는) 대단히 편리한 환경을 지원하지만 현재는 Rust에서만 작동합니다. 그룹은 다른 WebAssembly 타겟팅 언어에 대한 지원을 추가하는 것을 고려하고 있습니다.

Rust에서 슬라이스는 C에 있는 배열입니다. C에서와 마찬가지로 슬라이스를 가지고 있습니다. 이는 메모리 안전 모델에 어긋납니다. Rust가 적용하므로 unsafe 키워드를 사용해야 합니다. 해당 모델을 준수하지 않는 코드를 작성할 수 있습니다.

let imageSize = (inputWidth * inputHeight) as usize;
let inBuffer: &mut [u32];
let outBuffer: &mut [u32];
unsafe {
    inBuffer = slice::from_raw_parts_mut::<u32>(4 as *mut u32, imageSize);
    outBuffer = slice::from_raw_parts_mut::<u32>((imageSize * 4 + 4) as *mut u32, imageSize);
}

for d2 in 0..d2Limit {
    for d1 in 0..d1Limit {
    let in_idx = (d1Start + d1 * d1Advance) * d1Multiplier + (d2Start + d2 * d2Advance) * d2Multiplier;
    outBuffer[i as usize] = inBuffer[in_idx as usize];
    i += 1;
    }
}

다음을 사용하여 Rust 파일 컴파일

$ wasm-pack build

약 100바이트의 글루 코드가 포함된 7.6KB wasm 모듈을 생성합니다 (모두 gzip 후).

AssemblyScript

AssemblyScript는 TypeScript-WebAssembly 컴파일러를 목표로 하는 어린 프로젝트입니다. 그것은 그러나 TypeScript를 사용하지 않는다는 점에 유의해야 합니다. AssemblyScript는 TypeScript와 동일한 구문을 사용하지만 표준 사용할 수 있습니다 표준 라이브러리는 WebAssembly 즉, 거짓말을 하고 있는 TypeScript를 WebAssembly와 유사하지만 새로운 명령어를 프로그래밍 언어를 사용해 WebAssembly를 작성해 보겠습니다

    for (let d2 = d2Start; d2 >= 0 && d2 < d2Limit; d2 += d2Advance) {
      for (let d1 = d1Start; d1 >= 0 && d1 < d1Limit; d1 += d1Advance) {
        let in_idx = ((d1 * d1Multiplier) + (d2 * d2Multiplier));
        store<u32>(offset + i * 4 + 4, load<u32>(in_idx * 4 + 4));
        i += 1;
      }
    }

rotate() 함수에 있는 작은 서체 노출 영역을 고려할 때 이 코드를 AssemblyScript로 포팅하기 쉽습니다. load<T>(ptr: usize)store<T>(ptr: usize, value: T) 함수는 AssemblyScript에서 제공합니다. 원시 메모리에 액세스할 수 있습니다 AssemblyScript 파일을 컴파일하려면 다음 안내를 따르세요. AssemblyScript/assemblyscript npm 패키지만 설치하고

$ asc rotate.ts -b assemblyscript.wasm --validate -O3

AssemblyScript는 약 300바이트의 wasm 모듈을 제공하며 글루 코드는 없습니다. 이 모듈은 단순히 기본적인 WebAssembly API로 작동합니다.

WebAssembly 포렌식

Rust의 7.6KB는 다른 2개의 언어에 비해 놀라울 정도로 큽니다. 거기 이 도구는 WebAssembly 생태계에 있는 도구를 사용하여 제작 언어에 관계없이 WebAssembly 파일을 업데이트하고 상황을 알려주고 상황을 개선하는 데도 도움이 됩니다.

트위기

Twiggy는 Rust의 WebAssembly에서 다수의 유용한 데이터를 추출하는 WebAssembly 팀 모듈을 마칩니다 이 도구는 Rust 전용이 아니며 이를 사용하면 모듈의 호출 그래프를 확인하고, 사용되지 않거나 불필요한 섹션을 파악하여 모듈의 총 파일 크기에서 차지하는 비중이 어느 정도인지 확인하세요. 이 후자는 Twiggy의 top 명령어를 사용하여 실행할 수 있습니다.

$ twiggy top rotate_bg.wasm
Twiggy 설치 스크린샷

이 경우에는 대부분의 파일 크기가 있습니다. 코드가 동적 할당을 사용하지 않기 때문에 놀랍습니다. 또 다른 중요한 기여 요인은 '함수 이름' 하위 섹션을 참조하세요.

Wasm-스트립

wasm-stripWebAssembly 바이너리 툴킷(wabt)에 포함된 도구입니다. 여기에는 몇 가지 도구를 통해 WebAssembly 모듈을 검사하고 조작할 수 있습니다. wasm2wat는 바이너리 wasm 모듈을 인간이 읽을 수 있는 형식입니다. Wabt에는 wat2wasm가 포함되어 있어 그 인간이 읽을 수 있는 형식을 바이너리 wasm 모듈로 다시 보냅니다. 우리는 WebAssembly 파일을 검사하는 이 두 가지 보완 도구가 있는데, wasm-strip가 가장 유용합니다. wasm-strip에서 불필요한 섹션을 삭제함 및 메타데이터를 나열해야 합니다.

$ wasm-strip rotate_bg.wasm

이렇게 하면 Rust 모듈의 파일 크기가 7.5KB에서 6.6KB (gzip 후)로 줄어듭니다.

wasm-opt

wasm-optBinaryen의 도구입니다. WebAssembly 모듈을 취하여 크기 최적화와 성능을 향상시킵니다. Emscripten과 같은 일부 도구는 이미 일부에서는 그렇지 않습니다. 대개의 경우 문제가 없도록 추가 바이트 수를 줄일 수 있습니다.

wasm-opt -O3 -o rotate_bg_opt.wasm rotate_bg.wasm

wasm-opt를 사용하면 몇 바이트를 더 줄여 총 gzip 후 6.2KB

#![no_std]

약간의 상담과 연구 후, 저희는 이를 사용하지 않고 Rust 코드를 Rust의 표준 라이브러리로, #![no_std] 드림 기능을 사용할 수 있습니다. 이렇게 하면 동적 메모리 할당이 모두 사용 중지되므로 할당자 코드를 생성합니다. 이 Rust 파일 컴파일 다음 코드로 교체합니다.

$ rustc --target=wasm32-unknown-unknown -C opt-level=3 -o rust.wasm rotate.rs

wasm-opt, wasm-strip 및 gzip 이후에 1.6KB wasm 모듈을 생성했습니다. 하지만 C 및 AssemblyScript에서 생성된 모듈보다 여전히 크지만 경량으로 간주되기에 충분합니다

성능

파일 크기만을 기준으로 결론을 내리기 전에 이 여정을 진행했습니다. 파일 크기가 아닌 성능을 최적화하기 위해 그렇다면 실적을 어떻게 측정하고 결과는 어땠나요?

벤치마킹 방법

WebAssembly가 낮은 수준의 바이트 코드 형식임에도 불구하고 컴파일러를 통해 호스트별 기계어 코드를 생성합니다. 자바스크립트와 마찬가지로 컴파일러는 여러 단계에서 작동합니다 간단히 말해 첫 번째 단계는 컴파일은 더 빠르지만 더 느린 코드를 생성하는 경향이 있습니다. 모듈이 시작되면 브라우저는 자주 사용되는 부분을 관찰하고 최적화는 더 느리지만 속도가 느린 컴파일러를 통해 작동합니다

이 사용 사례는 이미지를 회전하는 코드가 이미지 회전을 위한 한 번, 두 번 정도요. 따라서 대부분의 경우 이점도 있습니다. 이것은 벤치마킹을 합니다. WebAssembly 모듈을 루프로 10,000번 실행하면 결과를 얻지 못할 수도 있습니다. 현실적인 숫자를 얻으려면 모듈을 한 번 실행하고 한 번의 실행에서 얻은 수치를 기반으로 의사 결정을 내립니다.

실적 비교

언어별 속도 비교
브라우저별 속도 비교

이 두 그래프는 동일한 데이터에 대한 서로 다른 뷰입니다. 첫 번째 그래프에서 두 번째 그래프에서는 사용된 언어별로 비교합니다. 제발 로그 시간 척도를 선택했습니다. 또한 모든 업계 기준치는 동일한 16메가픽셀 테스트 이미지와 동일한 호스트를 사용했습니다. 동일한 컴퓨터에서 실행할 수 없는 한 개의 브라우저를 제외하고 있습니다.

이 그래프를 너무 많이 분석하지 않으면 원래 문제를 해결했다는 것을 분명히 알 수 있습니다. 성능 문제: 모든 WebAssembly 모듈은 약 500ms 이내에 실행됩니다. 이 WebAssembly는 예측 가능한 정보를 제공하여 확인할 수 있습니다 어떤 언어를 선택하든 브라우저 간의 차이 언어를 최소화합니다 정확히 말하면 자바스크립트의 표준 편차는 표준 편차가 400ms인 반면, 모든 브라우저에서 WebAssembly 모듈의 속도가 80ms 미만입니다.

난이도

또 다른 측정항목은 시스템을 만들고 통합하는 데 좀 더 쉽게 이해할 수 있을 것입니다. 이 데이터에 숫자 값을 할당하는 것은 따라서 그래프를 만들지는 않겠지만, 몇 가지 예를 들어 요점:

AssemblyScript는 원활했습니다. TypeScript를 사용하여 WebAssembly를 작성하면 동료들이 코드 검토를 매우 쉽게 수행할 수 있을 뿐 아니라 는 접착이 없는 WebAssembly 모듈을 개발하는데, 이 모듈은 매우 작고 성능이 우수하며 확인할 수 있습니다 Prettier 및 tslint와 같은 TypeScript 생태계의 도구는 잘 될 것입니다.

Rust를 wasm-pack와 함께 사용하는 것도 매우 편리하지만 탁월합니다. 더 큰 규모의 WebAssembly 프로젝트에서는 바인딩이 더 많았고 메모리 관리는 확인할 수 있습니다 경쟁에서 앞서나가기 위해서는 지정할 수 있습니다.

C와 Emscripten은 매우 작고 성능이 뛰어난 WebAssembly 모듈을 만들었습니다. 그저 접착제 코드에 뛰어들어 이를 줄일 용기가 없다면 총 크기 (WebAssembly 모듈 + 글루 코드)가 반드시 필요합니다. 꽤 큽니다.

결론

따라서 JavaScript 핫 경로가 있고 이를 사용하려면 어떤 언어를 사용해야 할까요? 더 빠르게 또는 더 일관성 있게 사용할 수 있습니다. 언제나 그렇듯이 경우에 따라 다릅니다. 그래서 우리는 무엇을 배송했을까요?

<ph type="x-smartling-placeholder">
</ph> 비교 그래프

언어별 모듈 크기 / 성능 절충점 비교 C 또는 AssemblyScript를 선택하는 것이 가장 좋습니다. Rust를 제공하기로 결정했습니다. 거기 이 결정을 내리는 데는 여러 가지 이유가 있습니다. 지금까지 Squoosh에서 출시된 모든 코덱은 Emscripten을 사용하여 컴파일됩니다. 우리는 인공지능에 대한 지식을 넓히고자 WebAssembly 생태계를 확인하고 프로덕션에서 다른 언어를 사용하세요. AssemblyScript는 강력한 대안이지만, 이 프로젝트는 비교적 최근에 개발되었으며 컴파일러는 Rust 컴파일러만큼 성숙하지 않습니다.

Rust와 다른 언어 간의 파일 크기 차이는 분산형 그래프에서 꽤 급격하게 보이지만 실제로는 그렇게 큰 문제가 아닙니다. 2G로도 500B 또는 1.6KB를 로드하는 데 걸리는 시간은 1/10초도 채 되지 않습니다. 또한 Rust는 곧 모듈 크기 측면에서 이 격차를 메울 수 있기를 바랍니다.

런타임 성능 측면에서 Rust는 브라우저에서 AssemblyScript를 사용하세요. 특히 규모가 큰 프로젝트에서 Rust는 코드를 수동으로 최적화하지 않고도 더 빠른 코드를 생성할 수 있습니다. 하지만 가장 익숙한 방식을 사용하는 데 방해가 되어서는 안 됩니다.

다시 말해 AssemblyScript는 훌륭한 발견입니다. 웹 개발자가 WebAssembly 모듈을 개발하여 있습니다. AssemblyScript팀은 빠르게 대응하고 적극적으로 도구 모음을 개선하기 위해 노력하고 있습니다 Google에서는 AssemblyScript를 다시 사용할 것입니다.

업데이트: Rust

이 기사를 게시한 후 닉 피츠제럴드는 는 그들의 훌륭한 Rust Wasm 책을 지적해 주었고 파일 크기 최적화에 대한 섹션을 참조하세요. 지침 (특히 링크 시간 최적화 및 수동 작업, 패닉 처리)을 통해 '정상' Rust 코드를 작성하고 파일 크기를 팽창시키지 않는 Cargo (Rust의 npm). Rust 모듈이 종료됩니다. gzip 후에는 370B가 올라갑니다. 자세한 내용은 Squoosh에 공개한 PR을 참고하세요.

이 여정에 도움을 주신 애슐리 윌리엄스, 스티브 클래브니크, 닉 피츠제럴드, 맥스 그레이에게 특히 감사의 인사를 전합니다.