JIT 컴파일러의 단점: NYI를 피해야 하는 이유
API7.ai
September 30, 2022
이전 글에서 LuaJIT의 FFI를 살펴보았습니다. 만약 여러분의 프로젝트가 OpenResty가 제공하는 API만 사용하고 C 함수를 호출할 필요가 없다면, FFI는 그다지 중요하지 않습니다. 단지 lua-resty-core
가 활성화되어 있는지 확인하면 됩니다.
하지만 오늘 이야기할 LuaJIT의 NYI는 OpenResty를 사용하는 모든 엔지니어가 피할 수 없는 중요한 문제로, 성능에 큰 영향을 미칩니다.
OpenResty를 사용하면 논리적으로 올바른 코드를 빠르게 작성할 수 있지만, NYI를 이해하지 못하면 효율적인 코드를 작성할 수 없고 OpenResty의 힘을 제대로 활용할 수 없습니다. 두 가지의 성능 차이는 최소한 한 자릿수 이상입니다.
NYI란 무엇인가?
먼저, 이전에 언급했던 점을 다시 떠올려보겠습니다.
LuaJIT의 런타임은 Lua 인터프리터의 어셈블리 구현 외에도 JIT 컴파일러가 있어 기계어 코드를 직접 생성할 수 있습니다.
LuaJIT의 JIT 컴파일러 구현은 아직 완벽하지 않습니다. 일부 함수는 구현이 어렵고, LuaJIT 저자가 현재 반은 은퇴한 상태이기 때문에 컴파일할 수 없습니다. 이에는 흔히 사용되는 pairs()
함수, unpack()
함수, Lua CFunction 구현을 기반으로 한 Lua C 모듈 등이 포함됩니다. 이로 인해 JIT 컴파일러는 현재 코드 경로에서 지원하지 않는 연산을 만나면 인터프리터 모드로 돌아갑니다.
LuaJIT 공식 웹사이트에는 이러한 NYI의 전체 목록이 있으며, 이를 살펴보는 것을 권장합니다. 이 글의 목적은 이 목록을 외우는 것이 아니라, 코드를 작성할 때 이를 의식적으로 떠올리도록 하는 것입니다.
아래에서는 NYI 목록에서 문자열 라이브러리에 해당하는 몇 가지 함수를 가져왔습니다.
string.byte
의 컴파일 상태는 yes로, JIT로 최적화될 수 있음을 의미하며, 코드에서 두려움 없이 사용할 수 있습니다.
string.char
의 컴파일 상태는 2.1로, LuaJIT 2.1부터 지원되었음을 의미합니다. 우리가 알다시피 OpenResty의 LuaJIT는 LuaJIT 2.1을 기반으로 하므로 안전하게 사용할 수 있습니다.
string.dump
의 컴파일 상태는 never로, JIT로 최적화되지 않고 인터프리터 모드로 돌아갑니다. 현재로서는 앞으로도 지원할 계획이 없습니다.
string.find
의 컴파일 상태는 2.1 partial로, LuaJIT 2.1부터 부분적으로 지원되며, 주석에 따르면 고정 문자열 검색만 지원하고 패턴 매칭은 지원하지 않습니다. 따라서 고정 문자열을 찾는 경우 string.find
는 JIT로 최적화될 수 있습니다.
당연히 우리는 NYI를 피해야 더 많은 코드가 JIT 컴파일되고 성능이 보장될 수 있습니다. 그러나 실제 환경에서는 때로는 불가피하게 일부 NYI 함수를 사용해야 할 때가 있습니다. 그렇다면 어떻게 해야 할까요?
NYI의 대안
걱정하지 마세요. 대부분의 NYI 함수는 공손히 뒤로 미뤄두고 다른 방식으로 그 기능을 구현할 수 있습니다. 다음으로, 몇 가지 전형적인 NYI를 선택하여 설명하고 다양한 유형의 NYI 대안을 살펴보겠습니다. 이를 통해 다른 NYI에 대해서도 배울 수 있습니다.
string.gsub()
먼저 string.gsub()
함수를 살펴보겠습니다. 이 함수는 Lua의 내장 문자열 조작 함수로, 전역 문자열 치환을 수행합니다. 예를 들어 다음과 같습니다.
$ resty -e 'local new = string.gsub("banana", "a", "A"); print(new)'
bAnAnA
이 함수는 NYI 함수로, JIT로 컴파일될 수 없습니다.
OpenResty의 API에서 대체 함수를 찾아볼 수 있지만, 대부분의 사람들에게 모든 API와 그 사용법을 기억하는 것은 실용적이지 않습니다. 그래서 저는 개발 작업에서 항상 lua-nginx-module의 GitHub 문서 페이지를 열어둡니다.
예를 들어, gsub
를 키워드로 문서 페이지를 검색하면 ngx.re.gsub
가 떠오를 것입니다.
또한 이전에 추천한 restydoc
도구를 사용하여 OpenResty API를 검색할 수도 있습니다. gsub
를 검색해보세요.
$ restydoc -s gsub
보시다시피, 예상했던 ngx.re.gsub
대신 Lua의 함수가 표시됩니다. 사실 이 단계에서 restydoc
은 정확한 유일한 일치를 반환하므로, API 이름을 명확히 알고 있을 때 사용하기에 더 적합합니다. 퍼지 검색의 경우 여전히 문서에서 수동으로 해야 합니다.
검색 결과로 돌아가서, ngx.re.gsub
의 함수 정의는 다음과 같습니다.
newstr, n, err = ngx.re.gsub(subject, regex, replace, options?)
여기서 함수 매개변수와 반환 값은 특정 의미를 가진 이름으로 명명되었습니다. 사실 OpenResty에서는 많은 주석을 작성하는 것을 권장하지 않습니다. 대부분의 경우, 좋은 이름이 여러 줄의 주석보다 낫습니다.
OpenResty 정규식 시스템에 익숙하지 않은 엔지니어라면, 끝에 있는 options
변수를 보면 혼란스러울 수 있습니다. 그러나 이 변수에 대한 설명은 이 함수가 아니라 ngx.re.match
함수의 문서에 있습니다.
options에 대한 문서를 보면, 이를 jo
로 설정하면 PCRE JIT
가 활성화되어 ngx.re.gsub
를 사용하는 코드가 LuaJIT뿐만 아니라 PCRE JIT
로도 JIT 컴파일될 수 있습니다.
문서의 세부 사항은 다루지 않겠습니다. OpenResty 문서는 매우 훌륭하므로, 이를 주의 깊게 읽으면 대부분의 문제를 해결할 수 있습니다.
string.find()
string.gsub
와 달리 string.find
는 일반 모드(즉, 문자열 검색)에서 JIT 가능하지만, 정규식을 사용한 문자열 검색에서는 JIT 가능하지 않습니다. 이는 OpenResty의 API인 ngx.re.find
를 사용하여 수행됩니다.
따라서 OpenResty에서 문자열 검색을 할 때는 먼저 고정 문자열을 찾는지 정규식을 사용하는지 명확히 구분해야 합니다. 전자의 경우 string.find
를 사용하고, 끝에 plain
을 true
로 설정해야 합니다.
string.find("foo bar", "foo", 1, true)
후자의 경우 OpenResty의 API를 사용하고 PCRE의 JIT 옵션을 켜야 합니다.
ngx.re.find("foo bar", "^foo", "jo")
여기서 한 층 더 감싸고 최적화 옵션을 기본적으로 켜는 것이 더 적절할 것입니다. 최종 사용자가 이렇게 많은 세부 사항을 알 필요 없이 외부에서는 일관된 문자열 검색 함수로 보이게 하는 것입니다. 때로는 너무 많은 옵션과 유연성이 좋은 것이 아니라는 것을 느낄 수 있습니다.
unpack()
세 번째로 살펴볼 함수는 unpack()
입니다. unpack()
은 특히 루프 본문에서 피해야 할 함수입니다. 대신 배열의 인덱스 번호를 사용하여 접근할 수 있습니다. 다음 코드 예제를 참조하세요.
$ resty -e '
local a = {100, 200, 300, 400}
for i = 1, 2 do
print(unpack(a))
end'
$ resty -e 'local a = {100, 200, 300, 400}
for i = 1, 2 do
print(a[1], a[2], a[3], a[4])
end'
unpack
에 대해 조금 더 깊이 파고들어보겠습니다. 이번에는 restydoc
을 사용하여 검색할 수 있습니다.
$ restydoc -s unpack
unpack 문서에서 볼 수 있듯이, unpack(list [, i [, j]])
는 return list[i], list[i+1], list[j]
와 동일하며, unpack을 문법적 설탕으로 생각할 수 있습니다. 이렇게 하면 LuaJIT의 JIT 컴파일을 깨지 않고 배열 인덱스로 정확히 접근할 수 있습니다.
pairs()
마지막으로 해시 테이블을 순회하는 pairs()
함수를 살펴보겠습니다. 이 함수도 JIT로 컴파일될 수 없습니다.
하지만 불행히도 이에 대한 동등한 대안은 없습니다. 이를 피하거나 숫자 인덱스로 접근하는 배열을 사용해야 하며, 특히 핫 코드 경로에서 해시 테이블을 순회하지 않도록 해야 합니다. 여기서 핫 코드 경로란 코드가 여러 번 실행될 경로를 의미합니다. 예를 들어, 거대한 루프 내부가 될 수 있습니다.
이 네 가지 예를 통해 NYI 함수 사용을 피하기 위해 주의해야 할 두 가지 사항을 요약해보겠습니다.
- Lua의 표준 라이브러리 함수보다 OpenResty가 제공하는 API를 우선적으로 사용하세요. Lua는 임베디드 언어이며, 우리는 Lua가 아니라 OpenResty에서 프로그래밍하고 있다는 것을 기억하세요.
- 어쩔 수 없이 NYI 언어를 사용해야 한다면, 반드시 코드의 핫 경로에 있지 않도록 하세요.
NYI를 어떻게 감지할까?
NYI 회피에 대해 이렇게 많이 이야기한 것은 여러분이 무엇을 해야 하는지 가르치기 위함입니다. 그러나 여기서 갑자기 끝낸다면 OpenResty가 지지하는 철학 중 하나와 일치하지 않을 것입니다.
기계가 자동으로 할 수 있는 일은 사람이 개입하지 않아야 합니다.
사람은 기계가 아니며, 항상 실수가 있을 수 있습니다. 코드에서 사용된 NYI를 자동으로 감지하는 것은 엔지니어의 가치를 반영하는 중요한 부분입니다.
여기서 LuaJIT에 내장된 jit.dump
와 jit.v
모듈을 추천합니다. 둘 다 JIT 컴파일러가 어떻게 작동하는지 출력합니다. 전자는 상세한 정보를 출력하여 LuaJIT 자체를 디버깅하는 데 사용할 수 있습니다. 더 깊이 이해하려면 소스 코드를 참조하세요. 후자는 출력이 더 간단하며, 각 줄이 하나의 트레이스에 해당하며, 일반적으로 JIT 가능 여부를 확인하는 데 사용됩니다.
어떻게 해야 할까요? 먼저 init_by_lua
에 다음 두 줄의 코드를 추가할 수 있습니다.
local v = require "jit.v"
v.on("/tmp/jit.log")
그런 다음, 스트레스 테스트 도구나 몇 백 개의 단위 테스트 세트를 실행하여 LuaJIT가 충분히 뜨거워져 JIT 컴파일이 트리거되도록 합니다. 그런 다음 /tmp/jit.log
의 결과를 확인하세요.
물론 이 방법은 비교적 번거롭습니다. 따라서 간단하게 하고 싶다면 resty
만으로도 충분하며, OpenResty CLI에는 다음과 같은 옵션이 있습니다.
$resty -j v -e 'for i=1, 1000 do
local newstr, n, err = ngx.re.gsub("hello, world", "([a-z])[a-z]+", "[$0,$1]", "i")
end'
[TRACE 1 (command line -e):1 stitch C:107bc91fd]
[TRACE 2 (1/stitch) (command line -e):2 -> 1]
resty
에서 -j
는 LuaJIT 관련 옵션으로, 값 dump와 v는 각각 jit.dump
와 jit.v
모드를 켜는 것에 해당합니다.
jit.v
모듈의 출력에서 각 줄은 성공적으로 컴파일된 트레이스 객체입니다. 방금은 JIT 가능한 트레이스의 예였으며, NYI 함수가 발견되면 출력에 NYI임을 명시합니다. 다음 pairs
예제를 참조하세요.
$resty -j v -e 'local t = {}
for i=1,100 do
t[i] = i
end
for i=1, 1000 do
for j=1,1000 do
for k,v in pairs(t) do
--
end
end
end'
JIT로 컴파일될 수 없으므로 결과는 8번째 줄에 NYI 함수가 있음을 나타냅니다.
[TRACE 1 (command line -e):2 loop]
[TRACE --- (command line -e):7 -- NYI: bytecode 72 at (command line -e):8]
마치며
이번에는 OpenResty 성능 문제에 대해 처음으로 길게 이야기했습니다. NYI에 대한 이러한 최적화를 읽고 나서 어떻게 생각하시나요? 의견을 댓글로 남겨주세요.
마지막으로, string.find()
함수의 대안을 논의할 때 언급했던 것처럼, 한 층 더 감싸고 최적화 옵션을 기본적으로 켜는 것이 더 좋을 것이라는 생각을 남기겠습니다. 이제 이 작업을 여러분에게 맡기겠습니다.
답변을 댓글 섹션에 자유롭게 작성해주세요. 이 글을 동료와 친구들과 공유하여 함께 소통하고 발전해 나가시길 바랍니다.