대부분 비교는 Fil-C++로 컴파일한 Zef 인터프리터와 Yolo-C 컴파일러로 빌드한 다른 인터프리터들 사이에서 수행
원래 Zef 인터프리터
성능을 거의 고려하지 않고 작성됐으며, 성능을 의식한 선택은 두 가지만 있었다고 명시
값 표현
64비트 tagged value 사용
담을 수 있는 값은 double, 32비트 정수, Object*
double은 0x1000000000000 오프셋 방식으로 표현
JavaScriptCore에서 배운 기법으로 소개
문헌에서는 NuN tagging으로 지칭
정수와 포인터는 네이티브 표현 사용
포인터 값이 0x100000000보다 작지 않다는 가정에 의존
위험한 선택이라고 직접 명시
대안으로 정수에 0xffff000000000000 상위 비트 태그를 둘 수 있었다고 언급
이 표현으로 숫자 연산에서 비트 테스트 기반 빠른 경로 구현 가능
더 중요한 이점은 숫자에 대한 힙 할당 회피
인터프리터를 새로 만들 때는 기본 값 표현을 초기에 잘 선택하는 일이 중요하며, 이후 변경은 매우 어려움
동적 타입 언어 구현 출발점으로 32비트 또는 64비트 tagged value 제시
구현 언어 선택
최적화를 충분히 담아낼 수 있는 언어로 C++ 계열 선택
Java는 저수준 최적화 상한 때문에 선택하지 않겠다고 명시
Rust는 GC 언어 구현에 필요한 전역 가변 상태와 순환 참조를 갖는 힙 표현 때문에 선택하지 않겠다고 밝힘
다중 언어 구성을 감수하거나 많은 unsafe 코드를 허용하면 일부 또는 전체에 Rust 사용 가능성 언급
성능 엔지니어링 관점의 잘못된 선택
Fil-C++ 사용
빠르게 개발 가능했고 GC를 공짜로 제공
메모리 안전성 위반을 진단 정보와 스택 트레이스로 보고
정의되지 않은 동작이 없음
성능 비용은 보통 약 4배
재귀적 AST 워킹 인터프리터
여러 곳에서 오버라이드되는 virtual Node::evaluate 메서드 구조
문자열 남용
Get AST 노드가 변수 이름을 설명하는 std::string 저장
변수 접근마다 그 문자열 사용
해시테이블 남용
Get 실행 시 문자열 키로 std::unordered_map 조회
재귀 호출 체인 기반 스코프 탐색
거의 모든 구조의 중첩과 클로저 허용
함수 F 안의 클래스 A, 클래스 B 안의 함수 G 같은 중첩에서 A의 메서드는 A 필드, F 지역 변수, B 필드, G 지역 변수를 볼 수 있음
원래 구현은 이를 서로 다른 스코프 객체를 질의하는 C++ 재귀 함수들로 처리
원래 구현의 특성
잘못된 선택에도 불구하고 적은 코드로 꽤 복잡한 언어 인터프리터 구현 가능
가장 큰 모듈은 parser
나머지는 단순하고 명확한 편
초기 성능
원래 인터프리터는 CPython 3.10보다 35배 느림
Lua 5.4.7보다 80배 느림
QuickJS-ng 0.14.0보다 23배 느림
전체 최적화 진행표
표는 Zef Baseline부터 Zef Change #21: No Asserts, 그리고 Zef in Yolo-C++ 까지 성능 변화를 정리
비교 열은 vs Zef Baseline, vs Python 3.10, vs Lua 5.4.7, vs QuickJS-ng 0.14.0
최종 행 기준 Zef Change #21: No Asserts는 베이스라인 대비 16.646배 빠름
Python 3.10보다 2.13배 느림
Lua 5.4.7보다 4.781배 느림
QuickJS-ng 0.14.0보다 1.355배 느림
Zef in Yolo-C++는 베이스라인 대비66.962배 빠름
Python 3.10보다 1.889배 빠름
Lua 5.4.7보다 1.189배 느림
QuickJS-ng 0.14.0보다 2.968배 빠름
초기 최적화 단계
최적화 #1: 연산자 직접 호출
파서가 더 이상 연산자를 연산자 이름을 가진 DotCall 노드로 만들지 않고, 각 연산자별 별도 AST 노드 생성
Zef에서는 a + b와 a.add(b)가 동일
원래는 a + b를 DotCall(a, "add")와 인자 b 로 파싱
모든 산술 연산마다 연산자 메서드 이름 문자열 조회 발생
DotCall은 문자열을 Value::callMethod로 전달
Value::callMethod는 다중 문자열 비교 수행
변경 후 파서는 Binary<>, Unary<> 노드 생성
템플릿과 람다를 활용해 연산자별 서로 다른 Node::evaluate 오버라이드 제공
각 노드는 해당 연산자의 Value 빠른 경로 직접 호출
예시로 a + b는 Binary<lambda for add>::evaluate 호출 뒤 Value::add 호출
성능 효과는 17.5% 향상
이 시점 성능은 CPython 3.10보다 30배 느림
Lua 5.4.7보다 67배 느림
QuickJS-ng 0.14.0보다 19배 느림
최적화 #2: RMW 연산자 직접 호출
일반 연산자는 빨라졌지만, a += b 같은 RMW 형태는 여전히 문자열 기반 디스패치 사용
파서가 각 RMW 케이스용 별도 노드를 생성하도록 변경
파서는 LValue 노드가 makeRMW 가상 호출을 통해 자신을 RMW로 대체하도록 요청
RMW로 바뀌는 LValue는 Get, Dot, Subscript
Get은 변수 읽기 id 대응
Dot은 expr.id 대응
Subscript는 expr[index] 대응
각 가상 호출은 SPECIALIZE_NEW_RMW 매크로 사용
SetRMW는 id += value
DotSetRMW는 expr.id += value
SubscriptRMW는 expr[index] += value
변경 #1의 연산자 특수화는 람다 디스패치 사용
RMW는 enum 사용
get, dot, subscript 세 경로를 모두 처리하며 enum을 여러 위치에 전달해야 해서 선택
최종적으로 Value::callRMW<> 템플릿 함수가 실제 RMW 연산자 호출 디스패치 수행
성능 효과는 3.7% 향상
이 시점 성능은 CPython 3.10보다 29배 느림
Lua 5.4.7보다 65배 느림
QuickJS-ng 0.14.0보다 18.5배 느림
시작점 대비 1.22배 빠름
최적화 #3: IntObject 검사 회피
Value의 빠른 경로가 isInt() 를 사용하고, 그 내부 isIntSlow() 가 Object::isInt() 가상 호출을 수행하는 점이 병목
원래 값 표현에는 네 가지 경우 존재
tagged int32
tagged double
int32로 표현 불가능한 int64용 IntObject
그 외 모든 객체
IntObject 경우에도 정수 메서드 디스패치를 Value가 담당
모든 산술 연산 구현을 한 곳, 즉 Value에 두기 위함
최적화 후 Value 빠른 경로는 int32와 double만 고려
IntObject 처리 로직은 IntObject 자신으로 이동
메서드 디스패치마다 발생하던 isInt() 호출 회피
성능 효과는 1% 향상
이 시점 성능은 CPython 3.10보다 29배 느림
Lua 5.4.7보다 65배 느림
QuickJS-ng 0.14.0보다 18배 느림
시작점 대비 1.23배 빠름
최적화 #4: Symbol
원래 인터프리터는 std::string을 거의 모든 곳에 사용
비용이 큰 문자열 사용 위치는 Context::get, Context::set, Context::callFunction, Value::callMethod, Value::dot, Value::setDot, Value::callOperator<>, Object::callMethod 계열
이런 구조에서는 단순 해시테이블 조회가 아니라 문자열 키 해시테이블 조회가 되어 실행 중 문자열 해싱과 비교 반복
최적화는 문자열 기반 조회를 hash-consed Symbol 객체 포인터로 대체
새 Symbol 클래스 추가
symbol.h, symbol.cpp에 구현
Symbol과 문자열은 상호 변환 가능
문자열을 Symbol로 바꿀 때 전역 해시테이블로 hash consing 수행
결과적으로 Symbol* 포인터 동일성 비교만으로 같은 심볼 여부 판별
문자열 리터럴 대신 미리 준비된 심볼 사용
예시로 "subscript" 대신 Symbol::subscript
함수 시그니처 다수가 const std::string& 대신 Symbol* 사용으로 변경
성능 효과는 18% 향상
이 시점 성능은 CPython 3.10보다 24배 느림
Lua 5.4.7보다 54배 느림
QuickJS-ng 0.14.0보다 15배 느림
시작점 대비 1.46배 빠름
최적화 #5: Value 인라인화
핵심은 중요한 함수들의 인라인화 허용
거의 모든 변경의 중심은 새 헤더 valueinlines.h 도입
value.h와 별도 헤더로 분리한 이유는, value.h를 포함해야 하는 헤더들을 사용하기 때문
성능 효과는 2.8% 향상
이 시점 성능은 CPython 3.10보다 24배 느림
Lua 5.4.7보다 53배 느림
QuickJS-ng 0.14.0보다 15배 느림
시작점 대비 1.5배 빠름
객체 모델과 캐시 구조 재설계
최적화 #6: 객체 모델, 인라인 캐시, Watchpoint
Object, ClassObject, Context 작동 방식을 대규모 재구성해 객체 할당 비용을 낮추고 접근 시 해시테이블 조회 회피
이 변경은 객체 모델, 인라인 캐시, watchpoint 세 기능 결합
객체 모델
이전에는 각 렉시컬 스코프마다 Context 객체 할당
각 Context는 그 스코프 변수들을 담는 해시테이블 보유
객체는 더 복잡한 구조
각 객체가 자신이 인스턴스인 클래스들을 Context에 매핑하는 해시테이블 보유
이런 구조가 필요했던 이유는 상속과 중첩 스코프 때문
Bar가 Foo를 상속할 때 Bar와 Foo가 서로 다른 스코프를 클로즈오버
같은 이름의 서로 다른 private 필드도 가질 수 있음
새 구조는 Storage 개념 도입
데이터는 Offsets에 따라 저장
offset은 어떤 Context 가 결정
Context는 여전히 존재하지만 객체나 스코프 생성 시점이 아니라 AST의 resolve 패스에서 미리 생성
실제 객체나 스코프 생성 시에는 해당 Context가 계산한 크기에 맞춰 Storage만 할당
인라인 캐시
expr.name 같은 코드 위치에서 마지막으로 본 expr의 동적 타입과 name이 해석된 마지막 offset을 기억하는 기법
JIT 문맥에서 주로 설명되는 고전적 기법이지만, 여기서는 인터프리터에 적용
기억한 정보는 일반 AST 노드 위에 특수화된 AST 노드를 placement construct하는 방식으로 구현
인라인 캐시 구성 요소
CacheRecipe
특정 접근이 무엇을 했는지, 캐시 가능한지 추적
Context, ClassObject, Package 곳곳에 CacheRecipe 호출 삽입
접근 과정의 정보 수집
Dot::evaluate 같은 AST 평가 함수는 자신이 수행한 다형적 연산에서 얻은 CacheRecipe 를 this와 함께 constructCache<> 에 전달
constructCache
CacheRecipe에 따라 새 AST 노드 특수화 컴파일
템플릿 기계로 다양한 특수화 AST 노드 생성
지역 변수 접근이면 전달받은 storage에 대한 직접 로드
마지막으로 본 클래스와 동일한지 확인하는 class check
그 뒤 마지막으로 본 함수에 대한 직접 함수 호출
필요하면 chain step과 watchpoint 조합
캐싱 대상 AST 노드는 각각 cached variant 보유
먼저 cache 객체로 빠른 호출 시도
cache 객체 타입은 constructCache<>가 결정
watchpoint
렉시컬 스코프에 변수 x가 있고 그 안에 클래스 Foo가 있으며, Foo 메서드가 x에 접근하는 예시 제시
Foo 내부에 x라는 함수나 변수가 없다면 바로 바깥 x 를 읽을 수 있을 것처럼 보임
그러나 서브클래스가 getter x를 추가할 수 있음
그 경우 접근 결과는 바깥 x가 아니라 getter가 되어야 함
인라인 캐시는 이런 변경 가능성을 처리하기 위해 런타임에 Watchpoint 설정
예시에서는 이 이름이 오버라이드되었는지 감시하는 watchpoint 사용
세 기능을 동시에 구현한 이유
새 객체 모델만으로는 인라인 캐시가 잘 작동하지 않으면 의미 있는 개선이 어려움
인라인 캐시도 watchpoint가 없으면 많은 캐시 조건을 안전하게 다루기 어려워 실익이 작음
새 객체 모델과 watchpoint는 함께 잘 작동해야 함
구현 진행과 어려운 부분
시작은 단순한 CacheRecipe 버전 작성과, 최종 형태에 가까운 Storage, Offsets 설계부터 진행
가장 어려운 작업 중 하나는 intrinsic class 구현 방식 교체
배열 예시
이전에는 ArrayObject::tryCallMethod가 Object::tryCallMethod 가상 호출을 가로채는 방식으로 모든 메서드 구현
새 객체 모델에서는 Object에 vtable도 없고 가상 메서드도 없음
대신 Object::tryCallMethod는 object->classObject()->tryCallMethod(object, ...) 로 위임
따라서 Array 메서드를 제공하려면 그 메서드를 가진 Array용 클래스 자체 생성 필요
결과적으로 intrinsic 기능 상당 부분이 구현 전역에 흩어져 있던 구조에서 makerootcontext.cpp 중심으로 이동
긍정적인 결과로 평가한 이유는 객체의 native/intrinsic 함수에도 인라인 캐시가 그대로 적용되기 때문
성능 효과는 4.55배 향상
이 시점 성능은 CPython 3.10보다 5.2배 느림
Lua 5.4.7보다 11.7배 느림
QuickJS-ng 0.14.0보다 3.3배 느림
시작점 대비 6.8배 빠름
Fil-C++의 손실 폭이 다른 인터프리터 대비 대체로 Fil-C 비용 수준까지 좁혀졌다고 평가
호출과 접근 경로 최적화
최적화 #7: 인자 전달 구조 개선
변경 전 Zef 인터프리터는 함수 인자를 const std::optional<std::vector<Value>>& 로 전달
optional이 필요했던 이유는 일부 모서리 케이스에서 다음 둘을 구분해야 했기 때문
o.getter
o.function()
Zef에서는 대체로 둘 다 함수 호출이지만, 예외로 다음 코드 존재
o.NestedClass
o.NestedClass()
첫 번째는 NestedClass 객체 자체 반환
두 번째는 인스턴스 생성
따라서 인자가 없는 함수 호출과 getter류 호출로서 인자 배열이 비어 있는 경우를 구분해야 함
그러나 기존 구조는 비효율적
호출자가 vector 할당
피호출자가 그 벡터를 복사한 arguments scope를 다시 할당
변경은 Arguments 타입 도입
모양이 피호출자가 만들던 arguments scope와 정확히 동일
이제 호출자가 직접 그 형태로 할당
Yolo-C++에서도 vector backing store malloc 제거로 할당 수 감소
Fil-C++에서는 std::optional 자체가 힙 할당
std::optional이 없어도 const std::vector<>& 전달 역시 할당
스택 할당되는 것은 힙 할당된다고 명시
호출자 쪽이 벡터를 미리 크기 지정하지 않아 여러 번 재할당하던 점도 언급
변경의 상당 부분은 함수 시그니처를 Arguments* 로 교체하는 작업
성능 효과는 1.33배 향상
이 시점 성능은 CPython 3.10보다 3.9배 느림
Lua 5.4.7보다 8.8배 느림
QuickJS-ng 0.14.0보다 2.5배 느림
시작점 대비 9.05배 빠름
최적화 #8: Getter 특수화
Zef는 Ruby와 유사하게 인스턴스 필드가 기본적으로 private
예시 class Foo { my f fn (inF) f = inF }
생성자에서 받은 값을 인스턴스에만 보이는 지역 변수 f 에 저장
같은 타입 인스턴스라도 다른 객체의 f에는 접근할 수 없음
예시 fn nope(o) o.f
println(Foo(42).nope(Foo(666)))
nope 안의 o.f는 o의 f에 접근하지 못함
이유는 필드가 클래스 멤버의 스코프 체인에 나타나는 방식으로 작동하기 때문
o.f는 필드 읽기가 아니라 f라는 이름의 메서드 호출 요청
그래서 다음 패턴이 자주 등장
my f
fn f f
즉 지역 변수 f를 반환하는 이름 f의 메서드
더 짧은 문법으로 readable f
my f와 fn f f의 축약형
많은 메서드 호출이 실질적으로 getter 호출
모든 getter가 AST를 평가하며 동작하는 것은 낭비
최적화는 getter 특수화
중심은 UserFunction
새 Node::inferGetter 메서드로 함수 본문이 단순 getter인지 추론
추론 규칙
Block::inferGetter 는 자신이 담은 것이 전부 getter로 추론 가능하면 getter로 추론
Get::inferGetter 는 자신을 getter로 추론하고 로드할 offset 반환
Context::tryGetFieldOffsets 는 getter가 실행될 렉시컬 스코프에 그 필드가 확실히 존재할 때만 비어 있지 않은 Offsets 반환
UserFunction 은 함수 본문이 getter로 추론 가능하면 알려진 offset에서 바로 읽기만 수행하는 특수 Function 서브클래스로 resolve
성능 효과는 5.6% 향상
이 시점 성능은 CPython 3.10보다 3.7배 느림
Lua 5.4.7보다 8.3배 느림
QuickJS-ng 0.14.0보다 2.4배 느림
시작점 대비 9.55배 빠름
최적화 #9: Setter 특수화
setter 추론에서는 fn set_fieldName(newValue) fieldName = newValue 패턴 매칭 필요
UserFunction의 추론 단계에서 setter의 매개변수 이름 전달 필요
Set의 추론 단계에서는 ClassObject에 대한 쓰기 아님을 확인해야 하며, setter 매개변수가 set의 소스로 사용되는지도 함께 확인 필요
성능 효과는 3.4% 향상
이 시점 기준 Zef는 CPython 3.10보다 3.6배 느림
Lua 5.4.7보다 8배 느림
QuickJS-ng 0.14.0보다 2.3배 느림
시작점 대비 9.87배 빠름
최적화 #10: callMethod 인라인화
중요한 함수를 한 줄 변경으로 인라인화
성능 효과는 3.2% 향상
이 시점 기준 Zef는 CPython 3.10보다 3.5배 느림
Lua 5.4.7보다 7.8배 느림
QuickJS-ng 0.14.0보다 2.2배 느림
시작점 대비 10.2배 빠름
최적화 #11: 해시테이블
메서드 호출의 inline cache 미스가 발생하면 ClassObject::tryCallMethod와 ClassObject::TryCallMethodDirect를 따라 내려가야 했고, 두 경로 모두 크고 복잡
기존 탐색 비용은 계층 깊이에 비례하는 O(hierarchy depth)
계층의 각 클래스마다 호출이 멤버 함수로 해석되는지 확인하는 해시테이블 조회 수행
계층의 각 클래스마다 호출이 중첩 클래스로 해석되는지 확인하는 해시테이블 조회도 수행
새 변경으로 receiver class와 symbol을 키로 사용하는 전역 해시테이블 도입
한 번의 조회로 callee 직접 반환
classobject.h에서 전체 tryCallMethodSlow로 내려가기 전에 이 전역 테이블 먼저 조회
classobject.cpp에서는 성공한 조회 결과를 전역 테이블에 기록
전역 해시테이블 자체는 비교적 단순한 구현
성능 효과는 15% 향상
이 시점 기준 Zef는 CPython 3.10보다 3배 느림
Lua 5.4.7보다 6.8배 느림
QuickJS-ng 0.14.0보다 1.9배 느림
시작점 대비 11.8배 빠름
최적화 #12: std::optional 회피
Fil-C++에서는 union 관련 컴파일러 병리 현상 때문에 std::optional이 힙 할당 필요
일반적으로 LLVM은 union 메모리 접근 타입을 느슨하게 다루지만, 이것이 invisicaps와 충돌
union 안의 포인터가 프로그래머 입장에서 예측하기 어렵게 capability를 잃는 경우 발생
그 결과 Fil-C에서 프로그래머 실수 없이도 null capability를 가진 객체 역참조 패닉 발생
이를 완화하기 위해 Fil-C++ 컴파일러는 union 타입 지역 변수 처리에서 LLVM이 보수적으로 동작하도록 intrinsics 삽입
이후 FilPizlonator 패스가 자체 escape analysis를 수행해 union 타입 지역 변수를 레지스터 할당 가능하게 만들려 시도
다만 이 분석은 일반 LLVM의 SROA 분석만큼 완전하지 않음
결과적으로 std::optional처럼 union을 포함한 클래스 전달이 Fil-C++에서 메모리 할당으로 이어지는 경우가 많음
이번 변경은 hot path에서 std::optional로 이어지는 코드 경로 회피
성능 효과는 1.7% 향상
이 시점 기준 Zef는 CPython 3.10보다 3배 느림
Lua 5.4.7보다 6.65배 느림
QuickJS-ng 0.14.0보다 1.9배 느림
시작점 대비 12배 빠름
최적화 #13: 특수화된 인자
Zef의 모든 built-in 함수는 1개 또는 2개의 인자를 받으며, 네이티브 구현에서는 이를 담기 위한 Arguments 객체 할당 불필요
setter도 항상 한 개의 인자를 받으며, setter 추론이 이뤄진 경우 specialized setter 구현 역시 Arguments 객체 없이 값 인자만 직접 받으면 충분
이번 변경으로 ZeroArguments, OneArgument, TwoArguments 특수화 인자 타입 도입
callee가 필요로 하지 않는 경우 caller가 Arguments 객체 할당 회피
ZeroArguments는 (Arguments*)nullptr와 구분하기 위해 필요
기존에는 (Arguments*)nullptr를 getter 호출 의미로 사용했고, 그 로직 유지
이제 ZeroArguments는 인자 없는 함수 호출 의미
많은 변경이 인자를 받는 함수를 템플릿화하는 작업으로 구성
ZeroArguments, OneArgument, TwoArguments, Arguments* 각각에 대해 명시적 인스턴스화 수행
기존 코드 상당수는 Value::getArg를 인자 추출 헬퍼로 사용했고, 여기에 특수화 인자 오버로드 추가
인자를 사용하는 네이티브 코드 변경은 비교적 직선적인 수정
성능 효과는 3.8% 향상
이 시점 기준 Zef는 CPython 3.10보다 2.9배 느림
Lua 5.4.7보다 6.4배 느림
QuickJS-ng 0.14.0보다 1.8배 느림
시작점 대비 12.4배 빠름
Fil-C 병리 우회와 세부 특수화
최적화 #14: 개선된 Value slow path
또 다른 Fil-C 병리 현상 우회로 큰 속도 향상 확보
변경 전 Value의 out-of-line slow path는 Value의 멤버 함수였고, 암묵적인 const Value* 인자 필요
이 구조에서는 caller가 Value를 스택에 할당해야 함
Fil-C++에서는 모든 스택 할당이 힙 할당
따라서 slow path를 호출하는 코드는 Value를 힙에 할당
변경 후 이 메서드들을 static 으로 바꾸고, Value를 값으로 전달
그 결과 별도 할당 불필요
성능 효과는 10% 향상
이 시점 기준 Zef는 CPython 3.10보다 2.6배 느림
Lua 5.4.7보다 5.8배 느림
QuickJS-ng 0.14.0보다 1.65배 느림
시작점 대비 13.6배 빠름
최적화 #15: DotSetRMW 중복 제거
일부 중복 코드 제거 수행
constructCache<>에 의해 특수화되는 템플릿 함수에서 기계어 코드 감소가 유리할 수 있다고 기대
실제 결과는 성능 영향 없음
최적화 #16: sqrt 특수화
inline cache는 원하는 함수로 호출을 잘 라우팅하지만 객체에 대해서만 동작
비객체에서는 Binary<>, Unary<>, Value::callRMW<> fast path가 receiver가 int 또는 double인지 확인하는 방식에 의존
이 방식은 파서가 인식하는 연산자에만 적용
value.sqrt 같은 형태에는 적용되지 않음
이번 변경으로 Dot이 value.sqrt 에 대해 특수화 가능
성능 효과는 1.6% 향상
이 시점 기준 Zef는 CPython 3.10보다 2.6배 느림
Lua 5.4.7보다 5.75배 느림
QuickJS-ng 0.14.0보다 1.6배 느림
시작점 대비 13.8배 빠름
최적화 #17: toString 특수화
이전 최적화와 거의 동일한 방식으로 toString 특수화 적용
이 변경에는 int를 문자열로 변환할 때 발생하는 할당 수 감소 로직 포함
성능 효과는 2.7% 향상
이 시점 기준 Zef는 CPython 3.10보다 2.5배 느림
Lua 5.4.7보다 5.6배 느림
QuickJS-ng 0.14.0보다 1.6배 느림
시작점 대비 14.2배 빠름
최적화 #18: 배열 리터럴 특수화
my whatever = [1, 2, 3] 같은 코드는 Zef에서 배열이 alias 가능하고 mutable이므로 새 배열 할당 필요
변경 전에는 실행 때마다 AST를 따라 내려가며 1, 2, 3을 매번 재귀 평가
이번 변경은 ArrayLiteral 노드를 상수 배열 할당 경우에 특수화
성능 효과는 8.1% 향상
이 시점 기준 Zef는 CPython 3.10보다 2.3배 느림
Lua 5.4.7보다 5.2배 느림
QuickJS-ng 0.14.0보다 1.5배 느림
시작점 대비 15.35배 빠름
최적화 #19: Value::callOperator 개선
이전에 Value를 참조로 전달하지 않아 속도 향상을 얻었던 것과 같은 최적화를 callOperator slow path에도 적용
성능 효과는 6.5% 향상
이 시점 기준 Zef는 CPython 3.10보다 2.2배 느림
Lua 5.4.7보다 4.9배 느림
QuickJS-ng 0.14.0보다 1.4배 느림
시작점 대비 16.3배 빠름
최적화 #20: 더 나은 C++ 옵션
Fil-C++에서는 불필요한 RTTI와 libc++ hardening 비활성화
C++ 코드 자체 변경은 없고 빌드 시스템 설정 변경만 포함
성능 효과는 1.8% 향상
이 시점 기준 Zef는 CPython 3.10보다 2.1배 느림
Lua 5.4.7보다 4.8배 느림
QuickJS-ng 0.14.0보다 1.35배 느림
시작점 대비 16.6배 빠름
최적화 #21: assert 비활성화
마지막 최적화로 assertion 기본 비활성화 적용
기존 코드는 Fil-C 전용 ZASSERT 매크로 사용
항상 assert를 수행하는 구조
변경 후 내부 ASSERT 매크로 사용
ASSERTS_ENABLED가 설정된 경우에만 assert 수행
이 변경에는 코드가 Yolo-C++에서 빌드되도록 하는 다른 수정도 포함
기대와 달리 속도 향상 없음
Yolo-C++ 결과와 한계
코드를 Yolo-C++ 로 컴파일한 결과 4배 속도 향상 확보
다만 이 방식은 sound하지 않고 suboptimal
sound하지 않은 이유는 기존 Fil-C++ GC 호출이 calloc 호출로 바뀌기 때문
그 결과 메모리가 해제되지 않으며, 충분히 오래 실행되는 워크로드에서는 인터프리터가 메모리 고갈에 도달
ScriptBench1에서는 테스트 시간이 짧아 메모리 고갈 발생 없음
suboptimal한 이유는 실제 GC 할당기가 glibc 2.35의 calloc보다 빠르기 때문
따라서 Yolo-C++ 포트에 실제 GC를 추가하면 4배보다 더 큰 속도 향상 가능성 언급
이 실험에는 GCC 11.4.0 사용
이 시점 기준 Zef는
CPython 3.10보다 1.9배 빠름
Lua 5.4.7보다 1.2배 느림
QuickJS-ng 0.14.0보다 3배 빠름
시작점 대비 67배 빠름
원시 벤치마크 데이터
벤치마크 실행 시간 단위는 초
표에는 각 인터프리터별 nbody, splay, richards, deltablue, geomean 포함