오늘은 웹 어셈블리에 대해서 글을 써보려고 합니다.
간단히 웹 어셈블리가 뭔지 알아보고 Rust
를 사용해서 간단한 웹 어셈블리를 만들고 기존 Javascript와 어떤 점이 다른지 확인을 해보겠습니다.
웹 어셈블리란 ?
WebAssembly는 최신 웹 브라우저에서 실행할 수 있는 새로운 유형의 코드이며, 새로운 기능과 성능 면에서 큰 이점을 제공합니다. 직접 코드를 작성하는 것이 아니라 C, C++, RUST 등의 소스 언어를 효과적으로 컴파일하도록 고안되었습니다.
MDN 공식 문서
쉽게 말해 C, C++, Rust 등 빠른 언어를 컴파일하여 브라우저에서 실행시킬 수 있는 바이너리 형식으로 만드는 기술을 의미합니다.
웹 어셈블리, Javascript 처리과정 비교
웹 어셈블리와 Javascript의 처리 과정을 비교해 보겠습니다.
구문 해석
- javascript : 추상 구문 트리(AST)로 변환되어 이후 바이트 코드로 변환됩니다.
- 웹 어셈블리 : 이미 바이트 코드라서 변환이 필요 없고 decode를 하여 검증만 합니다.
컴파일 및 최적화
- javascript : JIT(Just in time) 컴파일러를 사용하여 컴파일을 합니다.
- 웹 어셈블리 : 컴파일러를 사용하여 많은 최적화가 진행된 상태라 최적화 과정이 적습니다.
재 최적화
- javascript : 기존의 최적화를 버리고 다시 최적화하는 경우가 있습니다.
- 웹 어셈블리 : 재 최적화가 발생하지 않습니다.
실행
- javascript : 컴파일러의 최적화 방식으로 최적화할 수 있지만 컴파일러의 내부동작을 알아야하고 브라우저마다 다른 최적화 방식으로 접근해야하기 때문에 쉽지 않습니다.
- 웹 어셈블리 : 컴파일러를 위해 최적화된만큼 빠릅니다.
가비지 컬렉션
- javascript : 가비지 컬렉터가 자동으로 메모리 관리를 해줍니다.
- 웹 어셈블리 : 메모리를 수동으로 관리합니다.
Rust로 웹 어셈블리를 만들어보자
저도 처음 다뤄보는 웹 어셈블리만큼 많이 낯설고 어려운 개념들인데요. 처음부터 깊게 다루기에는 무리가 있어 간단하게 만들어보면서 찍먹 해보겠습니다.
어떤 언어로 만들어볼까 고민하다가 전부터 배우고 싶었단 Rust
를 사용해서 만들어보겠습니다. (참고로 Rust도 할 줄 몰라 ChatGPT의 도움을 받겠습니다 ㅎ)
Rust 설치
우선 Rust를 설치해 보겠습니다. 공식문서에 친절하게 잘나와있네요. 이 글은 Mac OS기준으로 작성되었습니다.
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
이제 Rust가 설치되었을 겁니다. 잘 설치되었는지 확인해 볼까요 ?
cargo --version
잘 설치된 모습입니다.
Rust로 소스코드 작성
행렬의 곱을 계산해 주는 함수를 구현해 보겠습니다.
행렬에 대해서 몰라도 괜찮습니다. 그냥 복잡한 계산이라고 생각하시면 편합니다 !
use wasm_bindgen::prelude::*;
use rayon::prelude::*;
#[wasm_bindgen]
pub fn matrix_multiply(a: &[f64], b: &[f64], n: usize) -> Vec<f64> {
let mut result = vec![0.0; n * n];
result.par_chunks_mut(n).enumerate().for_each(|(i, row)| {
for k in 0..n {
let a_ik = a[i * n + k];
for j in 0..n {
row[j] += a_ik * b[k * n + j];
}
}
});
result
}
전지전능하신 chatGPT의 도움을 받아 작성했습니다.
Rust를 잘 몰라 다시 chatGPT에게 코드 분석을 요구하니
이런 식으로 응답이 옵니다.
위 코드는 Rust와 WebAssembly를 이용하여 행렬 곱셈을 수행하는 함수로, 병렬 처리를 통해 성능을 최적화하고 있습니다. 주요 구성 요소와 코드의 작동 방식을 단계별로 설명하겠습니다.
코드 분석
1. 의존성 및 모듈 가져오기
use wasm_bindgen::prelude::*;
use rayon::prelude::*;
wasm_bindgen::prelude::*
:- Rust 함수를 WebAssembly로 노출하기 위해
wasm_bindgen
매크로를 사용합니다.
- Rust 함수를 WebAssembly로 노출하기 위해
rayon::prelude::*
:- Rust에서 병렬 처리를 쉽게 구현할 수 있도록 도와주는
rayon
라이브러리의 프렐류드를 가져옵니다.
- Rust에서 병렬 처리를 쉽게 구현할 수 있도록 도와주는
2. 함수 정의
#[wasm_bindgen]
pub fn matrix_multiply(a: &[f64], b: &[f64], n: usize) -> Vec<f64> {
#[wasm_bindgen]
:- 이 매크로는 해당 함수를 WebAssembly로 노출하도록 합니다.
pub fn matrix_multiply(a: &[f64], b: &[f64], n: usize) -> Vec<f64>
:- 함수 시그니처입니다.
a
와b
는 입력 행렬이고,n
은 행렬의 크기입니다. 반환값은 결과 행렬입니다.
- 함수 시그니처입니다.
3. 결과 행렬 초기화
let mut result = vec![0.0; n * n];
result
:- 결과 행렬을 저장할 벡터입니다.
n * n
크기의 0.0으로 초기화된 벡터를 생성합니다.
- 결과 행렬을 저장할 벡터입니다.
4. 병렬 처리 루프
result.par_chunks_mut(n).enumerate().for_each(|(i, row)| {
for k in 0..n {
let a_ik = a[i * n + k];
for j in 0..n {
row[j] += a_ik * b[k * n + j];
}
}
});
result.par_chunks_mut(n)
:rayon
의par_chunks_mut
메서드를 사용하여 결과 벡터를 병렬로 처리할 수 있도록 합니다. 각 청크는n
개의 요소로 나뉩니다.
.enumerate().for_each(|(i, row)| {...})
:- 청크를 열거하고 각각에 대해 병렬 처리를 수행합니다.
i
는 행 인덱스이고,row
는 현재 처리 중인 결과 행렬의 행입니다.
- 청크를 열거하고 각각에 대해 병렬 처리를 수행합니다.
for k in 0..n
:- 내부 루프는 행렬 곱셈의 열 인덱스를 순회합니다.
let a_ik = a[i * n + k];
:- 입력 행렬
a
의 현재 행과 열 요소를 가져옵니다.
- 입력 행렬
for j in 0..n
:- 결과 행렬의 열 인덱스를 순회합니다.
row[j] += a_ik * b[k * n + j];
:- 행렬 곱셈의 누적 결과를 계산하여 결과 행렬의 해당 요소에 저장합니다.
5. 결과 반환
result
- 계산된 결과 행렬을 반환합니다.
결론
이 코드는 rayon
을 이용한 병렬 처리를 통해 행렬 곱셈을 수행합니다. 병렬 처리를 통해 성능을 최적화하고, Rust와 WebAssembly를 활용하여 웹 환경에서 효율적인 행렬 연산을 수행할 수 있도록 설계되었습니다. 주요 최적화는 par_chunks_mut
를 사용하여 결과 벡터를 병렬로 처리하는 부분에 있습니다. 이로 인해 행렬의 각 행을 병렬로 계산하여 성능을 향상시킵니다.
이제 이 함수를 웹 어셈블리로 컴파일을 해보겠습니다.
웹 어셈블리로 컴파일
먼저 이 명령어로
cargo install wasm-pack
Rust를 wasm으로 변환시켜주는 wasm-pack
을 설치를 해줍니다.
설치를 완료하고 이 명령어를 입력하면
wasm-pack build --target web
터미널에 이런 로그가 뜨면서
pkg
├── .gitignore
├── README.md
├── package.json
├── webassembly.d.ts
├── webassembly.js
├── webassembly_bg.wasm
└── webassembly_bg.wasm.d.ts
이런 구조의 파일들이 생겨나는 모습을 볼 수 있습니다. 이제 적용시켜보겠습니다.
웹 어셈블리 적용
vite로 간단한 React 프로젝트를 만들었습니다. 그리고 위의 빌드 결과물 중에 webassembly_bg.wasm, webassembly.js, webassembly.d.ts이것들만 빼서 React 프로젝트에 넣어줬습니다.
import { useEffect } from "react";
import "./App.css";
import init, { matrix_multiply } from "./webassembly/webassembly";
const n = 2000; // 행렬 크기
const a = new Float64Array(n * n).fill(1.0); // 예제 행렬 데이터
const b = new Float64Array(n * n).fill(1.0); // 예제 행렬 데이터
function App() {
useEffect(() => {
init(); // WebAssembly 모듈 초기화
}, []);
return (
<div>
<h1>WebAssembly vs JavaScript Matrix Multiplication</h1>
<button
onClick={() => {
const result = matrix_multiply(a, b, n);
console.log(result)
}}
>
WebAssembly Check
</button>
</div>
);
}
export default App;
이런 식으로 사용을 해서 버튼을 클릭하여 로그를 확인해 보면
로그가 잘 찍히는 것을 확인할 수 있습니다.
웹 어셈블리 VS Javascript
이제 Javascript로도 같은 로직을 구현해 웹 어셈블리랑 속도 차이를 비교해 보겠습니다.
우선 js로직도 만들어봐야겠죠 ? Rust로 짠 코드와 같이 행렬을 곱해주는 함수를 만들어 보겠습니다.
export function matrixMultiply(
a: Float64Array,
b: Float64Array,
n: number
): Float64Array {
const result = new Float64Array(n * n);
for (let i = 0; i < n; i++) {
for (let j = 0; j < n; j++) {
for (let k = 0; k < n; k++) {
result[i * n + j] += a[i * n + k] * b[k * n + j];
}
}
}
return result;
}
이제 이 함수도 실행시켜서 Rust로 만든 웹 어셈블리랑 같은 결과가 나오는지 확인해 보겠습니다.
결과가 잘 나오는 모습이네요 ! 이제 이 2개의 함수의 속도를 측정해서 비교해 보겠습니다.
import { useEffect } from "react";
import "./App.css";
import { matrixMultiply } from "./matrixMultiply";
import init, { matrix_multiply } from "./webassembly/webassembly";
const n = 2000; // 행렬 크기
const a = new Float64Array(n * n).fill(1.0); // 예제 행렬 데이터
const b = new Float64Array(n * n).fill(1.0); // 예제 행렬 데이터
function App() {
useEffect(() => {
init(); // WebAssembly 모듈 초기화
}, []);
return (
<div>
<h1>WebAssembly vs JavaScript Matrix Multiplication</h1>
<button
onClick={() => {
console.time("wasm matrix multiply");
matrix_multiply(a, b, n);
console.timeEnd("wasm matrix multiply");
}}
>
WebAssembly Check
</button>
<button
onClick={() => {
console.time("js matrix multiply");
matrixMultiply(a, b, n);
console.timeEnd("js matrix multiply");
}}
>
Javascript Check
</button>
</div>
);
}
export default App;
이런 식으로 코드를 구성하고 console.time, console.timeEnd를 사용해서 시간을 측정해 보겠습니다.
오... 상당한 차이가 보이네요.
뭔가 Rust에 대해서 좀 더 공부를 하고 잘 사용하면 정말 강력한 기능이 생길 거 같은데요. 오늘은 찍먹 느낌이었고 좀 더 공부해서 어떻게 잘 활용할지 연구를 해봐야겠습니다...!
이 글에서 사용했던 코드들입니다 :)
- Rust : https://github.com/hautest/rust-webassembly
- React : https://github.com/hautest/react-webassembly
참고자료
- https://tech.kakao.com/posts/438
- https://tecoble.techcourse.co.kr/post/2021-11-24-web-assembly/
- https://developer.mozilla.org/ko/docs/WebAssembly/Concepts#%EC%9E%90%EC%8B%A0%EC%9D%98_%EC%96%B4%ED%94%8C%EB%A6%AC%EC%BC%80%EC%9D%B4%EC%85%98%EC%97%90%EC%84%9C_webassembly%EB%A5%BC_%EC%96%B4%EB%96%BB%EA%B2%8C_%EC%8B%9C%EC%9E%91%ED%95%B4%EC%95%BC%ED%95%98%EB%82%98%EC%9A%94
'frontend' 카테고리의 다른 글
브라우저의 동작 원리 (0) | 2024.06.11 |
---|---|
(React 19 + Next js 15 + Panda Css) 1. 프로젝트 세팅 (1) | 2024.06.11 |
싱글스레드 언어인 JS에서는 Promise가 어떻게 동작할까 ? (0) | 2024.05.15 |
No.1 Css 라이브러리 "Panda Css" (0) | 2024.05.06 |
🚀 "우당탕탕 도서관" 프론트엔드 개발자 글쓰기 커뮤니티 2기 모집 안내 🚀 (0) | 2024.05.04 |