
🎯 수도쿠 문제를 Wasm과 자바스크립트로 각각 풀며, 성능 차이를 비교합니다.
해당 글에서는 C 코드를 wasm으로 변환하고, 변환한 코드로 연산을 수행한 후에 자바스크립트와 비교하는 과정을 담았습니다.
JavaScript
import { strToTwoDArr, twoDArrToStr } from "./";
type Problem = string;
type Board = string[][];
interface IsValid {
board: Board;
r: number;
c: number;
num: string;
}
export const sudokuSolve = (problem: Problem) => {
const board = strToTwoDArr(problem);
const len = board.length;
const divider = Math.floor(Math.sqrt(len));
const numbers = Array.from({ length: len }, (_, i) => {
if (i + 1 < 10) return String(i + 1);
else return String.fromCharCode(65 + (i + 1 - 10));
});
let isFinish = false;
const solve = () => {
if (isFinish) return;
const pos = emptyPos();
if (pos.length === 0) {
isFinish = true;
return;
}
const [r, c] = pos;
for (const num of numbers) {
if (isValid({ board, r, c, num })) {
board[r][c] = num;
solve();
if (isFinish) return;
board[r][c] = "0";
}
}
};
const emptyPos = () => {
for (let r = 0; r < len; r++) {
for (let c = 0; c < len; c++) {
if (board[r][c] === "0") return [r, c];
}
}
return [];
};
const isValid = ({ r, c, num }: IsValid) => {
for (let i = 0; i < len; i++) {
if (board[r][i] === num) {
return false;
}
}
for (let i = 0; i < len; i++) {
if (board[i][c] === num) {
return false;
}
}
const startRow = Math.floor(r / divider) * divider;
const startCol = Math.floor(c / divider) * divider;
for (let i = 0; i < divider; i++) {
for (let j = 0; j < divider; j++) {
if (board[startRow + i][startCol + j] === num) {
return false;
}
}
}
return true;
};
solve();
return twoDArrToStr(board);
};
C
#include <stdio.h>
#include <math.h>
#include <stdbool.h>
#include <string.h>
#include <emscripten/emscripten.h>
#define SIZE 16
typedef char Board[SIZE][SIZE];
EMSCRIPTEN_KEEPALIVE bool isValid(Board board, int r, int c, char num) {
int divider = sqrt(SIZE);
for (int i = 0; i < SIZE; i++) {
if (board[r][i] == num) {
return false;
}
}
for (int i = 0; i < SIZE; i++) {
if (board[i][c] == num) {
return false;
}
}
int startRow = floor(r / divider) * divider;
int startCol = floor(c / divider) * divider;
for (int i = 0; i < divider; i++) {
for (int j = 0; j < divider; j++) {
if (board[startRow + i][startCol + j] == num) {
return false;
}
}
}
return true;
}
EMSCRIPTEN_KEEPALIVE void emptyPos(Board board, int* r, int* c) {
for (*r = 0; *r < SIZE; (*r)++) {
for (*c = 0; *c < SIZE; (*c)++) {
if (board[*r][*c] == '0') return;
}
}
*r = -1;
*c = -1;
}
EMSCRIPTEN_KEEPALIVE bool solve(Board board) {
int r, c;
emptyPos(board, &r, &c);
if (r == -1) {
return true;
}
for (char num = '1'; num <= '9'; num++) {
if (isValid(board, r, c, num)) {
board[r][c] = num;
if (solve(board)) return true;
board[r][c] = '0';
}
}
for (char num = 'A'; num <= 'G'; num++) {
if (isValid(board, r, c, num)) {
board[r][c] = num;
if (solve(board)) return true;
board[r][c] = '0';
}
}
return false;
}
EMSCRIPTEN_KEEPALIVE char* sudoku_solve(char* input){
int len = sqrt(strlen(input));
Board initialBoard;
for (int i = 0; i < len; i++) {
for (int j = 0; j < len; j++) {
initialBoard[i][j] = '0';
}
}
for (int i = 0; i < strlen(input); i++) {
initialBoard[i / SIZE][i % SIZE] = input[i];
}
solve(initialBoard);
char* result = (char*)malloc((SIZE * SIZE * 3 + 1) * sizeof(char));
result[0] = '\0';
for (int i = 0; i < SIZE; i++) {
for (int j = 0; j < SIZE; j++) {
result[i*SIZE+j] = initialBoard[i][j];
}
}
return result;
}
int main() {
return 0;
}
컴파일 명령어
emcc --no-entry ./sudoku_solve.c -o ./sudoku_solve.js \
-O3 \
-s ENVIRONMENT='web' \
-s EXPORTED_FUNCTIONS='["_sudoku_solve"]' \
-s EXPORT_ES6=1 \
-s EXPORTED_RUNTIME_METHODS='["cwrap"]'
💡 cwrap은 C 함수를 호출하기 위한 JavaScript 함수를 생성하는데 사용된다. 이 함수는 C 함수와 JavaScript 코드 간의 인터페이스 역할을 수행한다.
cwrap의 매개변수는 총 4 가지다. 차례대로
solve.js
import Module from "../wasm/sudoku_solve.js";
export async function sudokuSolve(problem: string) {
const instance = await Module();
const sudoku_solve = instance.cwrap("sudoku_solve", "string", ["string"]);
return sudoku_solve(problem);
}
export const wasmWorker = new ComlinkWorker<typeof import("./solve.js")>(
new URL("./solve.js", import.meta.url)
);
(async function(){
await worker.sudokuSolve(problem);
})()

cpu 집약적인 일이 아니라면, Wasm을 사용하는 것을 지양 해야겠다. 일반적인 형태의 서비스를 제공하는 웹 어플리케이션이라면 웹 워커를 사용하는 것만으로도 UI 스레드의 유휴를 충분히 보장할 수 있다고 생각한다.
Wasm 사용이 꺼려지는 이유는 다음과 같다.
import Module from './excellent.wasm?init';
Module().then((instance)=>{
instance.exports.내함수();
});
// or
(async function(){
const instance = await Module();
const {내함수} = instance;
})()
import Module from './excellent.js';
(async function createFn(){
const instance = await Module()
const fn = instance.cwrap("add", "num", ["number","number"]);
fn(3,3); // 👈👈👈 6
fn(4,5); // 👈👈👈 9
})()
아주 간단한 C 코드를 js로 컴파일 하면 아래와 같이 500줄이 넘는 파일이 생성된다.

🎯 따라서, 정말 필요한 상황이 아니라면 webAssembly를 사용하지 않겠다.