[clang] [clang][bytecode] Use tailcalls via `[[clang::musttail]]` (PR #173756)
Timm Baeder via cfe-commits
cfe-commits at lists.llvm.org
Sat Dec 27 21:44:17 PST 2025
Timm =?utf-8?q?Bäder?= <tbaeder at redhat.com>,
Timm =?utf-8?q?Bäder?= <tbaeder at redhat.com>
Message-ID: <llvm.org/llvm/llvm-project/pull/173756 at github.com>
In-Reply-To:
https://github.com/tbaederr created https://github.com/llvm/llvm-project/pull/173756
This PR attempts to use a tailcall design for the bytecode interpreter instead of the giant-switch design, similar to [what Python has been doing](https://blog.reverberate.org/2025/02/10/tail-call-updates.html).
Using the two benchmarks from https://developers.redhat.com/articles/2025/10/15/clang-bytecode-interpreter-update, I get these numbers:
```
-- Sqlite
BASELINE:
Benchmark 1: bin/clang++ -c -std=c++26 sqlite.cpp -fconstexpr-steps=1000000000
Time (mean ± σ): 18.197 s ± 0.370 s [User: 17.926 s, System: 0.269 s]
Range (min … max): 17.657 s … 19.215 s 30 runs
BEFORE:
Benchmark 1: bin/clang++ -c -std=c++26 sqlite.cpp -fconstexpr-steps=1000000000 -fexperimental-new-constant-interpreter
Time (mean ± σ): 8.156 s ± 0.148 s [User: 7.881 s, System: 0.273 s]
Range (min … max): 7.909 s … 8.491 s 30 runs
AFTER:
Benchmark 1: bin/clang++ -c -std=c++26 sqlite.cpp -fconstexpr-steps=1000000000 -fexperimental-new-constant-interpreter
Time (mean ± σ): 7.492 s ± 0.171 s [User: 7.214 s, System: 0.277 s]
Range (min … max): 7.213 s … 7.876 s 30 runs
-- Heap allocation:
BASELINE:
Benchmark 1: bin/clang++ -c -std=c++26 test.cpp -fconstexpr-steps=1000000000
Time (mean ± σ): 28.650 s ± 0.475 s [User: 28.627 s, System: 0.021 s]
Range (min … max): 27.767 s … 29.554 s 30 runs
BEFORE:
Benchmark 1: bin/clang++ -c -std=c++26 test.cpp -fconstexpr-steps=1000000000 -fexperimental-new-constant-interpreter
Time (mean ± σ): 4.543 s ± 0.194 s [User: 4.527 s, System: 0.016 s]
Range (min … max): 4.340 s … 5.143 s 30 runs
AFTER:
Benchmark 1: bin/clang++ -c -std=c++26 test.cpp -fconstexpr-steps=1000000000 -fexperimental-new-constant-interpreter
Time (mean ± σ): 4.055 s ± 0.140 s [User: 4.039 s, System: 0.015 s]
Range (min … max): 3.827 s … 4.430 s 30 runs
```
However, the numbers on the compile-time tracker are slightly worse: https://llvm-compile-time-tracker.com/compare.php?from=699354a4146e2ebc7e7c7aa84d14c396a4300508&to=0b9191c493c35d33721c9aad7ab341fe2a731f3f&stat=instructions:u
I did this mostly for research purposes, but the result is kinda neat and I wonder if we should go this route. Not sure who to add as a reviewer for feedback and to check if I'm doing everything right.
>From 699354a4146e2ebc7e7c7aa84d14c396a4300508 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Timm=20B=C3=A4der?= <tbaeder at redhat.com>
Date: Fri, 26 Dec 2025 09:07:13 +0100
Subject: [PATCH 1/3] Test
---
clang/lib/AST/ExprConstant.cpp | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/clang/lib/AST/ExprConstant.cpp b/clang/lib/AST/ExprConstant.cpp
index f80dabf5444c7..01f7126f4c942 100644
--- a/clang/lib/AST/ExprConstant.cpp
+++ b/clang/lib/AST/ExprConstant.cpp
@@ -931,7 +931,7 @@ namespace {
: Ctx(const_cast<ASTContext &>(C)), EvalStatus(S), CurrentCall(nullptr),
CallStackDepth(0), NextCallIndex(1),
StepsLeft(C.getLangOpts().ConstexprStepLimit),
- EnableNewConstInterp(C.getLangOpts().EnableNewConstInterp),
+ EnableNewConstInterp(true),
BottomFrame(*this, SourceLocation(), /*Callee=*/nullptr,
/*This=*/nullptr,
/*CallExpr=*/nullptr, CallRef()),
>From 622f3b474519dc7a0e5713ac8247a64761417243 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Timm=20B=C3=A4der?= <tbaeder at redhat.com>
Date: Fri, 26 Dec 2025 09:06:45 +0100
Subject: [PATCH 2/3] musttail
---
clang/lib/AST/ByteCode/Interp.cpp | 44 +++++-----
clang/lib/AST/ByteCode/Interp.h | 7 +-
clang/utils/TableGen/ClangOpcodesEmitter.cpp | 87 ++++++++++++++++++++
3 files changed, 114 insertions(+), 24 deletions(-)
diff --git a/clang/lib/AST/ByteCode/Interp.cpp b/clang/lib/AST/ByteCode/Interp.cpp
index 889ac1e1a9a7e..af1bf76db865d 100644
--- a/clang/lib/AST/ByteCode/Interp.cpp
+++ b/clang/lib/AST/ByteCode/Interp.cpp
@@ -28,7 +28,8 @@
using namespace clang;
using namespace clang::interp;
-static bool RetValue(InterpState &S, CodePtr &Pt) {
+__attribute__((preserve_none)) static bool RetValue(InterpState &S,
+ CodePtr &Pt) {
llvm::report_fatal_error("Interpreter cannot return values");
}
@@ -2324,38 +2325,39 @@ bool FinishInitGlobal(InterpState &S, CodePtr OpPC) {
return true;
}
-// https://github.com/llvm/llvm-project/issues/102513
-#if defined(_MSC_VER) && !defined(__clang__) && !defined(NDEBUG)
-#pragma optimize("", off)
-#endif
+__attribute__((preserve_none)) static bool InterpNext(InterpState &S,
+ CodePtr &PC);
+
bool Interpret(InterpState &S) {
// The current stack frame when we started Interpret().
// This is being used by the ops to determine wheter
// to return from this function and thus terminate
// interpretation.
- const InterpFrame *StartFrame = S.Current;
assert(!S.Current->isRoot());
CodePtr PC = S.Current->getPC();
- // Empty program.
- if (!PC)
- return true;
+ return InterpNext(S, PC);
+}
- for (;;) {
- auto Op = PC.read<Opcode>();
- CodePtr OpPC = PC;
+#define GET_INTERPFNS_
+#include "Opcodes.inc"
+#undef GET_INTERPFNS_
- switch (Op) {
-#define GET_INTERP
+using InterpFn = __attribute__((preserve_none)) bool (*)(InterpState &,
+ CodePtr &PC);
+
+const InterpFn InterpFunctions[] = {
+#define GET_INTERPFNS
#include "Opcodes.inc"
-#undef GET_INTERP
- }
- }
+#undef GET_INTERPFNS
+};
+
+__attribute__((preserve_none)) static bool InterpNext(InterpState &S,
+ CodePtr &PC) {
+ auto Op = PC.read<Opcode>();
+ auto Fn = InterpFunctions[Op];
+ [[clang::musttail]] return Fn(S, PC);
}
-// https://github.com/llvm/llvm-project/issues/102513
-#if defined(_MSC_VER) && !defined(__clang__) && !defined(NDEBUG)
-#pragma optimize("", on)
-#endif
} // namespace interp
} // namespace clang
diff --git a/clang/lib/AST/ByteCode/Interp.h b/clang/lib/AST/ByteCode/Interp.h
index 9accbbc1605a9..c32ade5e125b0 100644
--- a/clang/lib/AST/ByteCode/Interp.h
+++ b/clang/lib/AST/ByteCode/Interp.h
@@ -221,7 +221,7 @@ void cleanupAfterFunctionCall(InterpState &S, CodePtr OpPC,
const Function *Func);
template <PrimType Name, class T = typename PrimConv<Name>::T>
-bool Ret(InterpState &S, CodePtr &PC) {
+__attribute__((preserve_none)) bool Ret(InterpState &S, CodePtr &PC) {
const T &Ret = S.Stk.pop<T>();
assert(S.Current);
@@ -243,7 +243,8 @@ bool Ret(InterpState &S, CodePtr &PC) {
return true;
}
-inline bool RetVoid(InterpState &S, CodePtr &PC) {
+__attribute__((preserve_none)) inline bool RetVoid(InterpState &S,
+ CodePtr &PC) {
assert(S.Current->getFrameOffset() == S.Stk.size() && "Invalid frame");
if (!S.checkingPotentialConstantExpression() || S.Current->Caller)
@@ -3040,7 +3041,7 @@ static inline bool ShiftFixedPoint(InterpState &S, CodePtr OpPC, bool Left) {
// NoRet
//===----------------------------------------------------------------------===//
-inline bool NoRet(InterpState &S, CodePtr OpPC) {
+__attribute__((preserve_none)) inline bool NoRet(InterpState &S, CodePtr OpPC) {
SourceLocation EndLoc = S.Current->getCallee()->getEndLoc();
S.FFDiag(EndLoc, diag::note_constexpr_no_return);
return false;
diff --git a/clang/utils/TableGen/ClangOpcodesEmitter.cpp b/clang/utils/TableGen/ClangOpcodesEmitter.cpp
index d26122aca46bd..84c4a673e133a 100644
--- a/clang/utils/TableGen/ClangOpcodesEmitter.cpp
+++ b/clang/utils/TableGen/ClangOpcodesEmitter.cpp
@@ -36,6 +36,8 @@ class ClangOpcodesEmitter {
/// Emits the switch case and the invocation in the interpreter.
void EmitInterp(raw_ostream &OS, StringRef N, const Record *R);
+ void EmitInterpFns(raw_ostream &OS, StringRef N, const Record *R);
+ void EmitInterpFns_(raw_ostream &OS, StringRef N, const Record *R);
/// Emits the disassembler.
void EmitDisasm(raw_ostream &OS, StringRef N, const Record *R);
@@ -92,6 +94,8 @@ void ClangOpcodesEmitter::run(raw_ostream &OS) {
EmitEnum(OS, N, Opcode);
EmitInterp(OS, N, Opcode);
+ EmitInterpFns(OS, N, Opcode);
+ EmitInterpFns_(OS, N, Opcode);
EmitDisasm(OS, N, Opcode);
EmitProto(OS, N, Opcode);
EmitGroup(OS, N, Opcode);
@@ -109,6 +113,89 @@ void ClangOpcodesEmitter::EmitEnum(raw_ostream &OS, StringRef N,
OS << "#endif\n";
}
+void ClangOpcodesEmitter::EmitInterpFns_(raw_ostream &OS, StringRef N,
+ const Record *R) {
+ OS << "#ifdef GET_INTERPFNS_\n";
+ Enumerate(R, N, [&](ArrayRef<const Record *> TS, const Twine &ID) {
+ OS << "__attribute__((preserve_none))\nstatic bool Interp_" << ID
+ << "(InterpState &S, CodePtr &PC) {\n";
+
+ bool CanReturn = R->getValueAsBit("CanReturn");
+ const auto &Args = R->getValueAsListOfDefs("Args");
+ bool ChangesPC = R->getValueAsBit("ChangesPC");
+
+ if (Args.empty()) {
+ if (CanReturn) {
+ OS << " [[clang::musttail]] return " << N;
+ PrintTypes(OS, TS);
+ OS << "(S, PC);\n";
+ OS << "}\n";
+ return;
+ }
+
+ // OS << "llvm::errs() << \"Calling \" << \"" << N << "\\n\";";//'\n';
+ OS << " if (!" << N;
+ PrintTypes(OS, TS);
+ OS << "(S, PC))\n";
+ OS << " return false;\n";
+ OS << "[[clang::musttail]] return InterpNext(S, PC);\n";
+ OS << "}\n";
+ return;
+ }
+
+ OS << "{\n";
+
+ if (!ChangesPC)
+ OS << " CodePtr OpPC = PC;\n";
+
+ // Emit calls to read arguments.
+ for (size_t I = 0, N = Args.size(); I < N; ++I) {
+ const auto *Arg = Args[I];
+ bool AsRef = Arg->getValueAsBit("AsRef");
+
+ if (AsRef)
+ OS << " const auto &V" << I;
+ else
+ OS << " const auto V" << I;
+ OS << " = ";
+ OS << "ReadArg<" << Arg->getValueAsString("Name") << ">(S, PC);\n";
+ }
+
+ OS << " if (!" << N;
+ PrintTypes(OS, TS);
+ OS << "(S";
+ // OS << ", OpPC";
+ if (ChangesPC)
+ OS << ", PC";
+ else
+ OS << ", OpPC";
+ for (size_t I = 0, N = Args.size(); I < N; ++I)
+ OS << ", V" << I;
+ OS << "))\n";
+ OS << " return false;\n";
+
+ OS << "}\n";
+
+ if (!CanReturn)
+ OS << "[[clang::musttail]] return InterpNext(S, PC);\n";
+ else
+ OS << " return true;\n";
+ // OS << " return false;\n";
+
+ OS << "}\n";
+ });
+ OS << "#endif\n";
+}
+
+void ClangOpcodesEmitter::EmitInterpFns(raw_ostream &OS, StringRef N,
+ const Record *R) {
+ OS << "#ifdef GET_INTERPFNS\n";
+ Enumerate(R, N, [&OS](ArrayRef<const Record *>, const Twine &ID) {
+ OS << "&Interp_" << ID << ",\n";
+ });
+ OS << "#endif\n";
+}
+
void ClangOpcodesEmitter::EmitInterp(raw_ostream &OS, StringRef N,
const Record *R) {
OS << "#ifdef GET_INTERP\n";
>From b6fcb5732d182d7cae12d407aa5c52073582bbb8 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Timm=20B=C3=A4der?= <tbaeder at redhat.com>
Date: Sun, 28 Dec 2025 06:37:20 +0100
Subject: [PATCH 3/3] Revert "Test"
This reverts commit 699354a4146e2ebc7e7c7aa84d14c396a4300508.
---
clang/lib/AST/ExprConstant.cpp | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/clang/lib/AST/ExprConstant.cpp b/clang/lib/AST/ExprConstant.cpp
index 01f7126f4c942..f80dabf5444c7 100644
--- a/clang/lib/AST/ExprConstant.cpp
+++ b/clang/lib/AST/ExprConstant.cpp
@@ -931,7 +931,7 @@ namespace {
: Ctx(const_cast<ASTContext &>(C)), EvalStatus(S), CurrentCall(nullptr),
CallStackDepth(0), NextCallIndex(1),
StepsLeft(C.getLangOpts().ConstexprStepLimit),
- EnableNewConstInterp(true),
+ EnableNewConstInterp(C.getLangOpts().EnableNewConstInterp),
BottomFrame(*this, SourceLocation(), /*Callee=*/nullptr,
/*This=*/nullptr,
/*CallExpr=*/nullptr, CallRef()),
More information about the cfe-commits
mailing list