동기와 비동기방식의 콜백성능 비교 테스트

function a(){a()} 와 같이 함수 자신을 무한으로 반복하는 모습을 실행하면 Maximum call stack size exceeded 라는 에러가 발생하게 된다.

이것은 함수가 또 함수를 호출하게될때 계속 call stack에 쌓이게 되는데 쌓일 수 있는 한계치에 닿았을때 이 에러를 터트리며 실행이 종료되게 되는것이다.

 

이 것을 해결할 방법으로 call stack에 쌓이지 않도록 실행하는 방법이 있다.

비동기적으로 다음에 수행할 함수를 호출하면 call stack에 쌓이지 않는다.

이렇게 했을때 수행속도에서 얻는 불이익이 있을지에 대한 테스트를 해보았다.

결과는 다음과 같다.

 

대략 테스트는 아래의 시간 이내에 끝나는것으로 확인됩니다
Chrome : 12278214회 재귀 | M1 Macmini Ram 16GB | 7초이내
Chrome :  8205116회 재귀 | iPhone13 Pro        | 3초이내
Safari :  7943177회 재귀 | M1 Macmini Ram 16GB | 3초이내
Safari :  8168045회 재귀 | iPhone13 Pro        | 3초이내

테스트는 오래 걸려 컴퓨터에 부하를 줄 수 있음을 주의해주세요.
응답이 오랫동안 없다면 브라우저를 꺼주세요.


 

테스트한 코드는 다음과 같다

bGV0IHByZXZMb29wID0gMDsKbGV0IG1heFJlY3Vyc2l2ZVNpemUgPSAwOwpsZXQgdGltZW91dCA9IDEwMDA7CmxldCB0aW1lb3V0cmF0aW8gPSAxMDsgLy8g67mE64+Z6riw6rCAIOuPmeq4sOuztOuLpCDsnbQg66eM7YG87J2YIOuwsOyImOunjO2BvCDripDrpqzrqbQg7YWM7Iqk7Yq4IOykkeuLqApsZXQgYXN5bmNUaW1lb3V0ID0gdGltZW91dCAqIHRpbWVvdXRyYXRpbzsKbGV0IHJlc3VsdCA9IHsgX3N5bmM6IDAsIF9hc3luYzogMCwgbG9nOiBbXSB9Owp7CiAgIC8vIOuPmeq4sOyerOq3gOy9nOuwsSDthYzsiqTtirgKICAgbGV0IGxvb3AgPSAwOwogICBsZXQgdGltZSA9IHBlcmZvcm1hbmNlLm5vdygpOwogICBjb25zdCBjYWxsRnVuY3Rpb24gPSBmID0+IHsgZigpIH0KICAgY29uc3QgYWJjID0gKCkgPT4gewogICAgICBsb29wKys7CiAgICAgIGlmIChwcmV2TG9vcCA8PSBsb29wKSB7IC8vIOustOydmOuvuO2VmOyngOunjCDrj5nsnbztlZwg7YWM7Iqk7Yq466W8IOychO2VtOyEnCDrhKPsnYwKICAgICAgICAgY2FsbEZ1bmN0aW9uKGFiYyk7CiAgICAgIH0KICAgfQogICB3aGlsZSAodHJ1ZSkgewogICAgICBpZiAoIShsb29wICUgKG1heFJlY3Vyc2l2ZVNpemUpKSkgeyAvLyDrrLTsnZjrr7jtlZjsp4Drp4wg64+Z7J287ZWcIO2FjOyKpO2KuOulvCDsnITtlbTshJwg64Sj7J2MCiAgICAgICAgIGlmIChwZXJmb3JtYW5jZS5ub3coKSAtIHRpbWUgPiB0aW1lb3V0KSB7CiAgICAgICAgICAgIGJyZWFrOwogICAgICAgICB9CiAgICAgIH0KICAgICAgdHJ5IHsgYWJjKCk7IH0gY2F0Y2ggeyB9OwogICB9CiAgIHJlc3VsdC5fc3luYyA9IE1hdGgucm91bmQocGVyZm9ybWFuY2Uubm93KCkgLSB0aW1lKTsKICAgcmVzdWx0LmxvZy5wdXNoKGDrj5nquLDsnqzqt4DsvZzrsLE6ICR7bG9vcH3tmowg7J6s6reA7JeQIOqxuOumsCDsi5zqsIQgJHtyZXN1bHQuX3N5bmN9bXNgKTsKICAgbGV0IF9sb29wID0gbG9vcDsKICAgbG9vcCA9IDA7CiAgIHRyeSB7IGFiYygpOyB9IGNhdGNoIHsgfQogICBtYXhSZWN1cnNpdmVTaXplID0gbG9vcDsKICAgcHJldkxvb3AgPSBfbG9vcDsKfQp7CiAgIC8vIOu5hOuPmeq4sChtaWNyb3Rhc2sp7J6s6reA7L2c67CxIO2FjOyKpO2KuAogICBsZXQgbG9vcCA9IDA7CiAgIGxldCB0aW1lID0gcGVyZm9ybWFuY2Uubm93KCk7CiAgIGNvbnN0IGFiYyA9ICgpID0+IHsKICAgICAgaWYgKCEobG9vcCAlIChtYXhSZWN1cnNpdmVTaXplKSkpIHsKICAgICAgICAgaWYgKHBlcmZvcm1hbmNlLm5vdygpIC0gdGltZSA+IGFzeW5jVGltZW91dCkgewogICAgICAgICAgICBkb2N1bWVudC5xdWVyeVNlbGVjdG9yKCcjcHJlRWwnKS5pbm5lclRleHQgPSBg7YOA7J6E7JWE7JuD7Jy866GcIO2FjOyKpO2KuCDspJHri6guXG7soIHslrTrj4Qg67mE64+Z6riw6rCAIOuPmeq4sOuztOuLpCAke3RpbWVvdXRyYXRpb33rsLAg7J207IOBIOuKkOuguOuNmCDsg4HtmansnoXri4jri6QuYDsKICAgICAgICAgICAgcmV0dXJuOwogICAgICAgICB9IC8vIOustOydmOuvuO2VmOyngOunjCDrj5nsnbztlZwg7YWM7Iqk7Yq466W8IOychO2VtOyEnCDrhKPsnYwKICAgICAgfQogICAgICBsb29wKys7CiAgICAgIGlmIChwcmV2TG9vcCA8PSBsb29wKSB7CiAgICAgICAgIHJlc3VsdC5fYXN5bmMgPSBNYXRoLnJvdW5kKHBlcmZvcm1hbmNlLm5vdygpIC0gdGltZSk7CiAgICAgICAgIHJlc3VsdC5sb2cucHVzaChg67mE64+Z6riwKG1pY3JvdGFzaynsnqzqt4DsvZzrsLE6ICR7bG9vcH3tmowg7J6s6reA7JeQIOqxuOumsCDsi5zqsIQgJHtyZXN1bHQuX2FzeW5jfW1zYCk7CiAgICAgICAgIHJlc3VsdC5sb2cucHVzaChg6rKw6rO8IOuPmeq4sOyerOq3gOy9nOuwseydtCDruYTrj5nquLDsnqzqt4DsvZzrsLHrs7Tri6QgJHsocmVzdWx0Ll9hc3luYyAvIHJlc3VsdC5fc3luYykudG9GaXhlZCgyKX3rsLAg67mg66W064ukYCk7CiAgICAgICAgIGRvY3VtZW50LnF1ZXJ5U2VsZWN0b3IoJyNwcmVFbCcpLmlubmVyVGV4dCA9IHJlc3VsdC5sb2cuam9pbignXG4nKTsKICAgICAgICAgcmV0dXJuOwogICAgICB9CiAgICAgIHF1ZXVlTWljcm90YXNrKGFiYyk7CiAgIH0KICAgYWJjKCk7Cn0=
let prevLoop = 0; let maxRecursiveSize = 0; let timeout = 1000; let timeoutratio = 10; // 비동기가 동기보다 이 만큼의 배수만큼 느리면 테스트 중단 let asyncTimeout = timeout * timeoutratio; let result = { _sync: 0, _async: 0, log: [] }; { // 동기재귀콜백 테스트 let loop = 0; let time = performance.now(); const callFunction = f => { f() } const abc = () => { loop++; if (prevLoop <= loop) { // 무의미하지만 동일한 테스트를 위해서 넣음 callFunction(abc); } } while (true) { if (!(loop % (maxRecursiveSize))) { // 무의미하지만 동일한 테스트를 위해서 넣음 if (performance.now() - time > timeout) { break; } } try { abc(); } catch { }; } result._sync = Math.round(performance.now() - time); result.log.push(`동기재귀콜백: ${loop}회 재귀에 걸린 시간 ${result._sync}ms`); let _loop = loop; loop = 0; try { abc(); } catch { } maxRecursiveSize = loop; prevLoop = _loop; } { // 비동기(microtask)재귀콜백 테스트 let loop = 0; let time = performance.now(); const abc = () => { if (!(loop % (maxRecursiveSize))) { if (performance.now() - time > asyncTimeout) { document.querySelector('#preEl').innerText = `타임아웃으로 테스트 중단.\n적어도 비동기가 동기보다 ${timeoutratio}배 이상 느렸던 상황입니다.`; return; } // 무의미하지만 동일한 테스트를 위해서 넣음 } loop++; if (prevLoop <= loop) { result._async = Math.round(performance.now() - time); result.log.push(`비동기(microtask)재귀콜백: ${loop}회 재귀에 걸린 시간 ${result._async}ms`); result.log.push(`결과 동기재귀콜백이 비동기재귀콜백보다 ${(result._async / result._sync).toFixed(2)}배 빠르다`); document.querySelector('#preEl').innerText = result.log.join('\n'); return; } queueMicrotask(abc); } abc(); }