Binaryen으로 Wasm 컴파일 및 최적화

Binaryen은 컴파일러 및 도구 모음임 C++로 작성된 WebAssembly용 인프라 라이브러리를 제공합니다. Kubernetes는 직관적이고 빠르며 효과적인 WebAssembly로 컴파일하는 데 도움이 됩니다. 이 게시물에서는 ExampleScript라는 합성 장난감 언어의 예시로 Binaryen.js API를 사용하는 JavaScript의 WebAssembly 모듈 이 과정에서는 모듈 생성, 모듈에 대한 함수 추가, 내보내기의 기본사항 함수를 호출합니다. 이렇게 하면 이 과정 전반에 걸쳐 실제 프로그래밍 언어를 WebAssembly로 컴파일하는 역학을 배울 것입니다. 또한 Binaryen.js 및 wasm-opt로 명령줄을 설치합니다.

Binaryen에 대한 배경 지식

Binaryen은 C API 단일 헤더에 포함할 수 있으며 JavaScript에서 사용됩니다. 또한 WebAssembly 양식을 작성하시기 바랍니다. 일반적인 제어 흐름 그래프 이를 선호하는 컴파일러에 해당합니다

중간 표현 (IR)은 최종 변환에 사용되는 데이터 구조 또는 컴파일러나 가상 머신에 의해 내부적으로 소스 코드를 나타냅니다. 바이너리 내부 IR은 간결한 데이터 구조를 사용하며 완전한 병렬 방식으로 설계됨 코드 생성 및 최적화를 수행할 수 있습니다. Binaryen의 IR WebAssembly의 하위 집합이기 때문에 WebAssembly로 컴파일됩니다.

Binaryen의 옵티마이저에는 코드 크기와 속도를 개선할 수 있는 많은 패스가 있습니다. 이러한 최적화의 목표는 Binaryen을 컴파일러로 사용할 수 있을 정도로 강력하게 만드는 것입니다. 자체 백엔드가 있습니다 여기에는 WebAssembly 관련 최적화( 범용 컴파일러는 하지 않을 수 있음). 이는 Wasm으로 생각할 수 있습니다. 축소하겠습니다.

AssemblyScript, Binaryen의 사용자 예시

Binaryen은 여러 프로젝트에서 사용됩니다. 예를 들면 AssemblyScript는 Binaryen을 사용하여 TypeScript와 유사한 언어에서 직접 WebAssembly로 컴파일합니다. 예시 사용해 보기 AssemblyScript 플레이그라운드에서 찾을 수 있습니다.

AssemblyScript 입력:

export function add(a: i32, b: i32): i32 {
  return a + b;
}

Binaryen에서 생성한 텍스트 형식의 해당 WebAssembly 코드:

(module
 (type $0 (func (param i32 i32) (result i32)))
 (memory $0 0)
 (export "add" (func $module/add))
 (export "memory" (memory $0))
 (func $module/add (param $0 i32) (param $1 i32) (result i32)
  local.get $0
  local.get $1
  i32.add
 )
)

이전 예를 기반으로 생성된 WebAssembly 코드가 표시된 AssemblyScript 플레이그라운드

Binaryen 도구 모음

Binaryen 도구 모음은 JavaScript 및 Python에 사용할 수 있는 여러 유용한 도구를 제공합니다. 사용할 수 있습니다. 이러한 도구의 하위 집합은 다음과 같습니다. 포함된 도구의 전체 목록 프로젝트의 README 파일에서 사용할 수 있습니다.

  • binaryen.js: Binaryen 메서드를 노출하는 독립형 JavaScript 라이브러리입니다. 대상: Wasm 모듈 생성 및 최적화 빌드는 npm의 binaryen.js를 참조하세요. (또는 GitHub 또는 unpkg)를 사용합니다.
  • wasm-opt: WebAssembly를 로드하고 Binaryen IR을 실행하는 명령줄 도구입니다. 전달합니다.
  • wasm-aswasm-dis: 조립 및 분해하는 명령줄 도구 WebAssembly
  • wasm-ctor-eval: 함수 (또는 함수 일부)를 실행할 수 있는 명령줄 도구입니다. 함수)를 사용해야 합니다.
  • wasm-metadce: 가변형 환경에서 Wasm 파일의 일부를 삭제하는 명령줄 도구 모듈의 사용 방법에 따라 다릅니다.
  • wasm-merge: 여러 Wasm 파일을 하나로 병합하는 명령줄 도구 파일을 내보내기할 때 해당 가져오기를 내보내기에 연결합니다. 좋아요 표시 번들러는 JavaScript용, Wasm용에는 있습니다.

WebAssembly로 컴파일

한 언어를 다른 언어로 컴파일하는 데는 일반적으로 여러 단계가 포함됩니다. 중요한 항목은 다음 목록에 나와 있습니다.

  • 어휘 분석: 소스 코드를 토큰으로 나눕니다.
  • 구문 분석: 추상 구문 트리를 만듭니다.
  • 의미 분석: 오류를 확인하고 언어 규칙을 시행합니다.
  • 중간 코드 생성: 좀 더 추상적인 표현을 만듭니다.
  • 코드 생성: 도착어로 번역합니다.
  • 타겟별 코드 최적화: 타겟에 맞춰 최적화합니다.

Unix 환경에서 컴파일에 자주 사용되는 도구는 lexyacc:

  • lex (어휘 분석 생성기): lex는 어휘를 생성하는 도구입니다. 분석기는 렉서 또는 스캐너로도 알려져 있습니다. 한 세트의 일반적인 표현식과 해당 액션을 입력으로 사용하고, 어휘 분석기입니다.
  • yacc (Yet Another Compiler Compiler): yacc은 Yet Another Compiler Compiler): 구문 분석을 위한 파서 단어의 공식적인 문법 설명이 프로그래밍 언어를 입력으로 사용하고 파서용 코드를 생성합니다. 파서 일반적으로 추상 구문 트리 소스 코드의 계층 구조를 나타냅니다.
를 통해 개인정보처리방침을 정의할 수 있습니다.

실제 사례

이 게시물의 범위를 고려할 때 전체 프로그래밍을 다루는 것은 불가능합니다. 따라서 편의상 매우 제한적이고 쓸모가 없는 것으로 샘플 스크립트라는 합성 프로그래밍 언어를 일반적인 작업을 수행하는 방법을 알아봅니다.

  • add() 함수를 작성하려면 모든 덧셈의 예를 코딩합니다. 예를 들면 다음과 같습니다. 2 + 3입니다.
  • multiply() 함수를 작성하려면 예를 들어 6 * 12를 작성합니다.

사전 경고에 따르면, 전혀 쓸모가 없지만 어휘 Analyzer를 단일 정규 표현식(/\d+\s*[\+\-\*\/]\s*\d+\s*/)이 될 수 있습니다.

다음으로, 파서가 있어야 합니다. 실제로 매우 단순화된 버전의 추상 구문 트리는 이름이 지정된 캡처 그룹: /(?<first_operand>\d+)\s*(?<operator>[\+\-\*\/])\s*(?<second_operand>\d+)/입니다.

ExampleScript 명령어는 한 줄에 하나씩 입력되므로 파서가 코드를 처리할 수 있습니다. 줄바꿈 문자를 사용하여 분할합니다. 이렇게 하면 첫 번째 이전 글머리 기호 목록의 세 단계, 즉 어휘 분석, 구문 분석시맨틱 분석이 포함됩니다. 이러한 단계의 코드는 확인할 수 있습니다.

export default class Parser {
  parse(input) {
    input = input.split(/\n/);
    if (!input.every((line) => /\d+\s*[\+\-\*\/]\s*\d+\s*/gm.test(line))) {
      throw new Error('Parse error');
    }

    return input.map((line) => {
      const { groups } =
        /(?<first_operand>\d+)\s*(?<operator>[\+\-\*\/])\s*(?<second_operand>\d+)/gm.exec(
          line,
        );
      return {
        firstOperand: Number(groups.first_operand),
        operator: groups.operator,
        secondOperand: Number(groups.second_operand),
      };
    });
  }
}

중간 코드 생성

이제 ExampleScript 프로그램을 추상 구문 트리로 표현할 수 있습니다. 다음 단계는 추상화를 사용하여 추상화를 만드는 것입니다. 중간 표현을 사용합니다. 첫 번째 단계는 Binaryen에서 새 모듈을 만듭니다.

const module = new binaryen.Module();

추상 구문 트리의 각 줄에는 firstOperand, operator, secondOperand 가능한 네 가지 모델 유형 각각에 대해 연산자(예: +, -, *, /)를 모듈에 함수를 추가해야 함 Binaryen의 Module#addFunction() 메서드를 사용합니다. 매개변수 Module#addFunction() 메서드는 다음과 같습니다.

  • name: string로, 함수의 이름을 나타냅니다.
  • functionType: Signature로, 함수의 서명을 나타냅니다.
  • varTypes: Type[]로, 지정된 순서대로 추가 로컬을 나타냅니다.
  • body: Expression(함수의 콘텐츠)

긴장을 풀고 분류해야 할 더 많은 세부 사항이 있습니다. Binaryen 문서 이 공간을 탐색하는 데 도움이 될 수 있지만 결국 ExampleScript의 + 연산자를 사용하면 Module#i32.add() 메서드가 여러 개 중 하나입니다. 사용 가능 정수 연산. 덧셈에는 두 개의 피연산자, 즉 첫 번째 합계와 두 번째 합계가 필요합니다. 대상 함수를 실제로 호출할 수 있으려면 내보냄 Module#addFunctionExport().

module.addFunction(
  'add', // name: string
  binaryen.createType([binaryen.i32, binaryen.i32]), // params: Type
  binaryen.i32, // results: Type
  [binaryen.i32], // vars: Type[]
  //  body: ExpressionRef
  module.block(null, [
    module.local.set(
      2,
      module.i32.add(
        module.local.get(0, binaryen.i32),
        module.local.get(1, binaryen.i32),
      ),
    ),
    module.return(module.local.get(2, binaryen.i32)),
  ]),
);
module.addFunctionExport('add', 'add');

추상 구문 트리를 처리한 후 모듈에는 정수로 작동하는 3, 즉 Module#i32.add()에 기반한 add() subtract() 기준: Module#i32.sub(), multiply() 기준 Module#i32.mul()Module#f64.div()에 기반한 이상점 divide() ExampleScript는 부동 소수점 결과에서도 작동하기 때문입니다.

for (const line of parsed) {
      const { firstOperand, operator, secondOperand } = line;

      if (operator === '+') {
        module.addFunction(
          'add', // name: string
          binaryen.createType([binaryen.i32, binaryen.i32]), // params: Type
          binaryen.i32, // results: Type
          [binaryen.i32], // vars: Type[]
          //  body: ExpressionRef
          module.block(null, [
            module.local.set(
              2,
              module.i32.add(
                module.local.get(0, binaryen.i32),
                module.local.get(1, binaryen.i32)
              )
            ),
            module.return(module.local.get(2, binaryen.i32)),
          ])
        );
        module.addFunctionExport('add', 'add');
      } else if (operator === '-') {
        module.subtractFunction(
          // Skipped for brevity.
        )
      } else if (operator === '*') {
          // Skipped for brevity.
      }
      // And so on for all other operators, namely `-`, `*`, and `/`.

실제 코드베이스를 다루는 경우 때로는 가 호출됩니다. 데드 코드를 인위적으로 도입 (최적화하고 다음 단계에서 제거됨) ExampleScript의 실행 중인 예제에서 컴파일하고 내보내지 않은 함수를 추가하면 작업이 수행됩니다.

// This function is added, but not exported,
// so it's effectively dead code.
module.addFunction(
  'deadcode', // name: string
  binaryen.createType([binaryen.i32, binaryen.i32]), // params: Type
  binaryen.i32, // results: Type
  [binaryen.i32], // vars: Type[]
  //  body: ExpressionRef
  module.block(null, [
    module.local.set(
      2,
      module.i32.div_u(
        module.local.get(0, binaryen.i32),
        module.local.get(1, binaryen.i32),
      ),
    ),
    module.return(module.local.get(2, binaryen.i32)),
  ]),
);

이제 컴파일러가 거의 준비되었습니다. 반드시 필요한 것은 아니지만 가장 좋은 방법은 모듈의 유효성을 검사하며 Module#validate() 메서드 사용

if (!module.validate()) {
  throw new Error('Validation error');
}

결과 Wasm 코드 가져오기

받는사람 결과 Wasm 코드를 얻습니다. Binaryen에는 텍스트 표현 S-expression.wat 파일로 저장 사람이 읽을 수 있는 형식으로 작성해야 하며 이진 표현 브라우저에서 직접 실행할 수 있는 .wasm 파일로 저장됩니다. 바이너리 코드는 브라우저에서 직접 실행할 수 있습니다. 작동하는지 확인하기 위해 내보내기를 로깅하면 도움이 됩니다.

const textData = module.emitText();
console.log(textData);

const wasmData = module.emitBinary();
const compiled = new WebAssembly.Module(wasmData);
const instance = new WebAssembly.Instance(compiled, {});
console.log('Wasm exports:\n', instance.exports);

4개 모두가 포함된 ExampleScript 프로그램의 완전한 텍스트 표현 작업은 아래에 나열되어 있습니다. 데드 코드가 여전히 존재하는 것에 주목하세요. 다음 스크린샷에 따라 노출되지는 않습니다. WebAssembly.Module.exports()

(module
 (type $0 (func (param i32 i32) (result i32)))
 (type $1 (func (param f64 f64) (result f64)))
 (export "add" (func $add))
 (export "subtract" (func $subtract))
 (export "multiply" (func $multiply))
 (export "divide" (func $divide))
 (func $add (param $0 i32) (param $1 i32) (result i32)
  (local $2 i32)
  (local.set $2
   (i32.add
    (local.get $0)
    (local.get $1)
   )
  )
  (return
   (local.get $2)
  )
 )
 (func $subtract (param $0 i32) (param $1 i32) (result i32)
  (local $2 i32)
  (local.set $2
   (i32.sub
    (local.get $0)
    (local.get $1)
   )
  )
  (return
   (local.get $2)
  )
 )
 (func $multiply (param $0 i32) (param $1 i32) (result i32)
  (local $2 i32)
  (local.set $2
   (i32.mul
    (local.get $0)
    (local.get $1)
   )
  )
  (return
   (local.get $2)
  )
 )
 (func $divide (param $0 f64) (param $1 f64) (result f64)
  (local $2 f64)
  (local.set $2
   (f64.div
    (local.get $0)
    (local.get $1)
   )
  )
  (return
   (local.get $2)
  )
 )
 (func $deadcode (param $0 i32) (param $1 i32) (result i32)
  (local $2 i32)
  (local.set $2
   (i32.div_u
    (local.get $0)
    (local.get $1)
   )
  )
  (return
   (local.get $2)
  )
 )
)

더하기, 나누기, 곱하기, 빼기의 네 가지 함수 (노출되지 않은 데드 코드 제외)가 표시된 WebAssembly 모듈 내보내기의 DevTools 콘솔 스크린샷

WebAssembly 최적화

Binaryen에서는 Wasm 코드를 최적화하는 두 가지 방법을 제공합니다. 하나는 Binaryen.js 자체에 있습니다. 다른 하나는 명령줄용입니다 전자는 표준 최적화 집합을 적용합니다. 이를 통해 최적화 및 축소 수준을 설정할 수 있으며, 후자는 기본적으로 규칙을 사용하지 않지만 대신 전체 맞춤설정을 허용하므로 충분한 실험을 통해 타겟 사용자에 맞춰 코드를 기반으로 최적의 결과를 도출합니다.

Binaryen.js로 최적화

Binaryen으로 Wasm 모듈을 최적화하는 가장 간단한 방법은 Binaryen.js의 Module#optimize() 메서드를 직접 호출합니다. 설정 최적화 및 축소 수준을 제공합니다.

// Assume the `wast` variable contains a Wasm program.
const module = binaryen.parseText(wast);
binaryen.setOptimizeLevel(2);
binaryen.setShrinkLevel(1);
// This corresponds to the `-Os` setting.
module.optimize();

이렇게 하면 이전에 인위적으로 도입한 데드 코드가 제거되므로 ExampleScript 장난감 예의 Wasm 버전 텍스트 표현 더 이상 포함하지 않습니다. local.set/get 쌍이 최적화 단계 SimplifyLocals (기타 로컬 관련 최적화) 및 진공청소기 (분명히 불필요한 코드 삭제) 및 return에 의해 삭제됩니다. RemoveUnusedBrs (필요 없는 위치에서 휴식 시간 삭제)

 (module
 (type $0 (func (param i32 i32) (result i32)))
 (type $1 (func (param f64 f64) (result f64)))
 (export "add" (func $add))
 (export "subtract" (func $subtract))
 (export "multiply" (func $multiply))
 (export "divide" (func $divide))
 (func $add (; has Stack IR ;) (param $0 i32) (param $1 i32) (result i32)
  (i32.add
   (local.get $0)
   (local.get $1)
  )
 )
 (func $subtract (; has Stack IR ;) (param $0 i32) (param $1 i32) (result i32)
  (i32.sub
   (local.get $0)
   (local.get $1)
  )
 )
 (func $multiply (; has Stack IR ;) (param $0 i32) (param $1 i32) (result i32)
  (i32.mul
   (local.get $0)
   (local.get $1)
  )
 )
 (func $divide (; has Stack IR ;) (param $0 f64) (param $1 f64) (result f64)
  (f64.div
   (local.get $0)
   (local.get $1)
  )
 )
)

다양한 최적화 통과, Module#optimize()는 특정 최적화 및 축소 수준을 사용합니다. 기본값 있습니다. 전체 맞춤설정을 위해서는 wasm-opt 명령줄 도구를 사용해야 합니다.

wasm-opt 명령줄 도구를 사용하여 최적화

사용할 패스를 완전히 맞춤설정할 수 있도록 Binaryen에는 wasm-opt 명령줄 도구 다음을 얻는 방법은 다음과 같습니다. 사용 가능한 최적화 옵션의 전체 목록 도구의 도움말 메시지를 확인하세요. wasm-opt 도구는 아마도 가장 인기 있는 도구일 것입니다 여러 컴파일러 툴체인에서 Wasm 코드를 최적화하는 데 사용합니다. 여기에는 Emscripten, J2CL, Kotlin/Wasm dart2wasm, Wasm-pack

wasm-opt --help

패스에 대해 알 수 있도록, 오늘 배운 몇 가지를 전문 지식 없이도 이해할 수 있음:

  • CodeFolding: 코드를 병합하여 중복을 방지합니다 (예: 두 개의 if가 팔에는 끝부분에 안내가 공유되어 있습니다).
  • DeadArgumentElimination: 인수 삭제를 위한 연결 시간 최적화 패스 이 함수가 항상 동일한 상수로 호출되는 경우에 한합니다.
  • MinifyImportsAndExports: "a", "b"로 축소합니다.
  • DeadCodeElimination: 데드 코드를 삭제합니다.

최적화 설명서 다양한 플래그 중 어느 것이 더 나은지를 식별하기 위한 몇 가지 팁을 사용할 수 있습니다. 먼저 시도해 볼 가치가 있습니다 예를 들어 wasm-opt를 실행하는 경우가 있습니다. 입력을 더 축소합니다. 이러한 경우 다음 코드로 교체합니다. --converge 플래그 더 이상 최적화가 수행되지 않고 고정점이 발생할 때까지 반복을 계속합니다. 도달할 수 있습니다

데모

이 게시물에서 소개된 개념을 실제로 보려면 데모에서는 예시용 스크립트 입력을 제공할 수 있습니다. 또한 데모의 소스 코드 보기

결론

Binaryen은 WebAssembly 및 코드를 최적화할 수 있습니다. JavaScript 라이브러리 및 명령줄 도구 유연성과 사용 편의성을 제공합니다. 이 게시물은 데이터 애널리스트의 Binaryen의 효과와 잠재력을 강조한 Wasm 컴파일 극대화합니다. Binaryen의 맞춤설정을 위한 많은 옵션은 Wasm의 내부 요소에 대한 심층적인 지식이 필요합니다. 기본 설정이 이미 잘 작동합니다. 이렇게 하면 Binaryen과 함께!

감사의 말씀

이 게시물은 알론 자카이님이 검토했습니다. 토마스 라이블리레이첼 앤드류.