Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit c29bbe2

Browse files
GH-125498: Update JIT builds to use LLVM 19 and preserve_none (GH-125499)
1 parent 597d814 commit c29bbe2

File tree

11 files changed

+69
-78
lines changed

11 files changed

+69
-78
lines changed

.github/workflows/jit.yml

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ jobs:
6161
- true
6262
- false
6363
llvm:
64-
- 18
64+
- 19
6565
include:
6666
- target: i686-pc-windows-msvc/msvc
6767
architecture: Win32
@@ -121,10 +121,15 @@ jobs:
121121
choco install llvm --allow-downgrade --no-progress --version ${{ matrix.llvm }}.1.0
122122
./PCbuild/build.bat --experimental-jit ${{ matrix.debug && '-d' || '' }} -p ${{ matrix.architecture }}
123123
124+
# The `find` line is required as a result of https://github.com/actions/runner-images/issues/9966.
125+
# This is a bug in the macOS runner image where the pre-installed Python is installed in the same
126+
# directory as the Homebrew Python, which causes the build to fail for macos-13. This line removes
127+
# the symlink to the pre-installed Python so that the Homebrew Python is used instead.
124128
- name: Native macOS
125129
if: runner.os == 'macOS'
126130
run: |
127131
brew update
132+
find /usr/local/bin -lname '*/Library/Frameworks/Python.framework/*' -delete
128133
brew install llvm@${{ matrix.llvm }}
129134
SDKROOT="$(xcrun --show-sdk-path)" \
130135
./configure --enable-experimental-jit ${{ matrix.debug && '--with-pydebug' || '--enable-optimizations --with-lto' }}
@@ -165,15 +170,19 @@ jobs:
165170
name: Free-Threaded (Debug)
166171
needs: interpreter
167172
runs-on: ubuntu-latest
173+
strategy:
174+
matrix:
175+
llvm:
176+
- 19
168177
steps:
169178
- uses: actions/checkout@v4
170179
- uses: actions/setup-python@v5
171180
with:
172181
python-version: '3.11'
173182
- name: Build with JIT enabled and GIL disabled
174183
run: |
175-
sudo bash -c "$(wget -O - https://apt.llvm.org/llvm.sh)" ./llvm.sh 18
176-
export PATH="$(llvm-config-18 --bindir):$PATH"
184+
sudo bash -c "$(wget -O - https://apt.llvm.org/llvm.sh)" ./llvm.sh ${{ matrix.llvm }}
185+
export PATH="$(llvm-config-${{ matrix.llvm }} --bindir):$PATH"
177186
./configure --enable-experimental-jit --with-pydebug --disable-gil
178187
make all --jobs 4
179188
- name: Run tests
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Update JIT compilation to use LLVM 19
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
The JIT has been updated to leverage Clang 19’s new ``preserve_none`` attribute,
2+
which supports more platforms and is more useful than LLVM's existing ``ghccc``
3+
calling convention. This also removes the need to manually patch the calling
4+
convention in LLVM IR, simplifying the JIT compilation process.

Tools/jit/README.md

Lines changed: 9 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,49 +7,46 @@ This version of CPython can be built with an experimental just-in-time compiler[
77

88
The JIT compiler does not require end users to install any third-party dependencies, but part of it must be *built* using LLVM[^why-llvm]. You are *not* required to build the rest of CPython using LLVM, or even the same version of LLVM (in fact, this is uncommon).
99

10-
LLVM version 18 is required. Both `clang` and `llvm-readobj` need to be installed and discoverable (version suffixes, like `clang-18`, are okay). It's highly recommended that you also have `llvm-objdump` available, since this allows the build script to dump human-readable assembly for the generated code.
10+
LLVM version 19 is required. Both `clang` and `llvm-readobj` need to be installed and discoverable (version suffixes, like `clang-19`, are okay). It's highly recommended that you also have `llvm-objdump` available, since this allows the build script to dump human-readable assembly for the generated code.
1111

1212
It's easy to install all of the required tools:
1313

1414
### Linux
1515

16-
Install LLVM 18 on Ubuntu/Debian:
16+
Install LLVM 19 on Ubuntu/Debian:
1717

1818
```sh
1919
wget https://apt.llvm.org/llvm.sh
2020
chmod +x llvm.sh
21-
sudo ./llvm.sh 18
21+
sudo ./llvm.sh 19
2222
```
2323

24-
Install LLVM 18 on Fedora Linux 40 or newer:
24+
Install LLVM 19 on Fedora Linux 40 or newer:
2525

2626
```sh
27-
sudo dnf install 'clang(major) = 18' 'llvm(major) = 18'
27+
sudo dnf install 'clang(major) = 19' 'llvm(major) = 19'
2828
```
2929

3030
### macOS
3131

32-
Install LLVM 18 with [Homebrew](https://brew.sh):
32+
Install LLVM 19 with [Homebrew](https://brew.sh):
3333

3434
```sh
35-
brew install llvm@18
35+
brew install llvm@19
3636
```
3737

3838
Homebrew won't add any of the tools to your `$PATH`. That's okay; the build script knows how to find them.
3939

4040
### Windows
4141

42-
Install LLVM 18 [by searching for it on LLVM's GitHub releases page](https://github.com/llvm/llvm-project/releases?q=18), clicking on "Assets", downloading the appropriate Windows installer for your platform (likely the file ending with `-win64.exe`), and running it. **When installing, be sure to select the option labeled "Add LLVM to the system PATH".**
42+
Install LLVM 19 [by searching for it on LLVM's GitHub releases page](https://github.com/llvm/llvm-project/releases?q=19), clicking on "Assets", downloading the appropriate Windows installer for your platform (likely the file ending with `-win64.exe`), and running it. **When installing, be sure to select the option labeled "Add LLVM to the system PATH".**
4343

4444
Alternatively, you can use [chocolatey](https://chocolatey.org):
4545

4646
```sh
47-
choco install llvm --version=18.1.6
47+
choco install llvm --version=19.1.0
4848
```
4949

50-
### Dev Containers
51-
52-
If you are working CPython in a [Codespaces instance](https://devguide.python.org/getting-started/setup-building/#using-codespaces), there's no need to install LLVM as the Fedora 40 base image includes LLVM 18 out of the box.
5350

5451
## Building
5552

Tools/jit/_llvm.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
import subprocess
99
import typing
1010

11-
_LLVM_VERSION = 18
11+
_LLVM_VERSION = 19
1212
_LLVM_VERSION_PATTERN = re.compile(rf"version\s+{_LLVM_VERSION}\.\d+\.\d+\S*\s+")
1313

1414
_P = typing.ParamSpec("_P")

Tools/jit/_stencils.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import dataclasses
44
import enum
5+
import sys
56
import typing
67

78
import _schema
@@ -132,15 +133,26 @@ class Hole:
132133
def __post_init__(self) -> None:
133134
self.func = _PATCH_FUNCS[self.kind]
134135

135-
def fold(self, other: typing.Self) -> typing.Self | None:
136+
def fold(self, other: typing.Self, body: bytes) -> typing.Self | None:
136137
"""Combine two holes into a single hole, if possible."""
138+
instruction_a = int.from_bytes(
139+
body[self.offset : self.offset + 4], byteorder=sys.byteorder
140+
)
141+
instruction_b = int.from_bytes(
142+
body[other.offset : other.offset + 4], byteorder=sys.byteorder
143+
)
144+
reg_a = instruction_a & 0b11111
145+
reg_b1 = instruction_b & 0b11111
146+
reg_b2 = (instruction_b >> 5) & 0b11111
147+
137148
if (
138149
self.offset + 4 == other.offset
139150
and self.value == other.value
140151
and self.symbol == other.symbol
141152
and self.addend == other.addend
142153
and self.func == "patch_aarch64_21rx"
143154
and other.func == "patch_aarch64_12x"
155+
and reg_a == reg_b1 == reg_b2
144156
):
145157
# These can *only* be properly relaxed when they appear together and
146158
# patch the same value:

Tools/jit/_targets.py

Lines changed: 15 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@
2626
PYTHON_EXECUTOR_CASES_C_H = CPYTHON / "Python" / "executor_cases.c.h"
2727
TOOLS_JIT_TEMPLATE_C = TOOLS_JIT / "template.c"
2828

29-
3029
_S = typing.TypeVar("_S", _schema.COFFSection, _schema.ELFSection, _schema.MachOSection)
3130
_R = typing.TypeVar(
3231
"_R", _schema.COFFRelocation, _schema.ELFRelocation, _schema.MachORelocation
@@ -39,7 +38,6 @@ class _Target(typing.Generic[_S, _R]):
3938
_: dataclasses.KW_ONLY
4039
alignment: int = 1
4140
args: typing.Sequence[str] = ()
42-
ghccc: bool = False
4341
prefix: str = ""
4442
stable: bool = False
4543
debug: bool = False
@@ -88,11 +86,7 @@ async def _parse(self, path: pathlib.Path) -> _stencils.StencilGroup:
8886
sections: list[dict[typing.Literal["Section"], _S]] = json.loads(output)
8987
for wrapped_section in sections:
9088
self._handle_section(wrapped_section["Section"], group)
91-
# The trampoline's entry point is just named "_ENTRY", since on some
92-
# platforms we later assume that any function starting with "_JIT_" uses
93-
# the GHC calling convention:
94-
entry_symbol = "_JIT_ENTRY" if "_JIT_ENTRY" in group.symbols else "_ENTRY"
95-
assert group.symbols[entry_symbol] == (_stencils.HoleValue.CODE, 0)
89+
assert group.symbols["_JIT_ENTRY"] == (_stencils.HoleValue.CODE, 0)
9690
if group.data.body:
9791
line = f"0: {str(bytes(group.data.body)).removeprefix('b')}"
9892
group.data.disassembly.append(line)
@@ -112,9 +106,6 @@ def _handle_relocation(
112106
async def _compile(
113107
self, opname: str, c: pathlib.Path, tempdir: pathlib.Path
114108
) -> _stencils.StencilGroup:
115-
# "Compile" the trampoline to an empty stencil group if it's not needed:
116-
if opname == "trampoline" and not self.ghccc:
117-
return _stencils.StencilGroup()
118109
o = tempdir / f"{opname}.o"
119110
args = [
120111
f"--target={self.triple}",
@@ -128,6 +119,7 @@ async def _compile(
128119
f"-I{CPYTHON / 'Include' / 'internal'}",
129120
f"-I{CPYTHON / 'Include' / 'internal' / 'mimalloc'}",
130121
f"-I{CPYTHON / 'Python'}",
122+
f"-I{CPYTHON / 'Tools' / 'jit'}",
131123
"-O3",
132124
"-c",
133125
# This debug info isn't necessary, and bloats out the JIT'ed code.
@@ -143,44 +135,12 @@ async def _compile(
143135
# Don't call stack-smashing canaries that we can't find or patch:
144136
"-fno-stack-protector",
145137
"-std=c11",
138+
"-o",
139+
f"{o}",
140+
f"{c}",
146141
*self.args,
147142
]
148-
if self.ghccc:
149-
# This is a bit of an ugly workaround, but it makes the code much
150-
# smaller and faster, so it's worth it. We want to use the GHC
151-
# calling convention, but Clang doesn't support it. So, we *first*
152-
# compile the code to LLVM IR, perform some text replacements on the
153-
# IR to change the calling convention(!), and then compile *that*.
154-
# Once we have access to Clang 19, we can get rid of this and use
155-
# __attribute__((preserve_none)) directly in the C code instead:
156-
ll = tempdir / f"{opname}.ll"
157-
args_ll = args + [
158-
# -fomit-frame-pointer is necessary because the GHC calling
159-
# convention uses RBP to pass arguments:
160-
"-S",
161-
"-emit-llvm",
162-
"-fomit-frame-pointer",
163-
"-o",
164-
f"{ll}",
165-
f"{c}",
166-
]
167-
await _llvm.run("clang", args_ll, echo=self.verbose)
168-
ir = ll.read_text()
169-
# This handles declarations, definitions, and calls to named symbols
170-
# starting with "_JIT_":
171-
ir = re.sub(
172-
r"(((noalias|nonnull|noundef) )*ptr @_JIT_\w+\()", r"ghccc \1", ir
173-
)
174-
# This handles calls to anonymous callees, since anything with
175-
# "musttail" needs to use the same calling convention:
176-
ir = ir.replace("musttail call", "musttail call ghccc")
177-
# Sometimes *both* replacements happen at the same site, so fix it:
178-
ir = ir.replace("ghccc ghccc", "ghccc")
179-
ll.write_text(ir)
180-
args_o = args + ["-Wno-unused-command-line-argument", "-o", f"{o}", f"{ll}"]
181-
else:
182-
args_o = args + ["-o", f"{o}", f"{c}"]
183-
await _llvm.run("clang", args_o, echo=self.verbose)
143+
await _llvm.run("clang", args, echo=self.verbose)
184144
return await self._parse(o)
185145

186146
async def _build_stencils(self) -> dict[str, _stencils.StencilGroup]:
@@ -519,7 +479,6 @@ def _handle_relocation(
519479

520480
def get_target(host: str) -> _COFF | _ELF | _MachO:
521481
"""Build a _Target for the given host "triple" and options."""
522-
# ghccc currently crashes Clang when combined with musttail on aarch64. :(
523482
target: _COFF | _ELF | _MachO
524483
if re.fullmatch(r"aarch64-apple-darwin.*", host):
525484
target = _MachO(host, alignment=8, prefix="_")
@@ -535,16 +494,20 @@ def get_target(host: str) -> _COFF | _ELF | _MachO:
535494
]
536495
target = _ELF(host, alignment=8, args=args)
537496
elif re.fullmatch(r"i686-pc-windows-msvc", host):
538-
args = ["-DPy_NO_ENABLE_SHARED"]
539-
target = _COFF(host, args=args, ghccc=True, prefix="_")
497+
args = [
498+
"-DPy_NO_ENABLE_SHARED",
499+
# __attribute__((preserve_none)) is not supported
500+
"-Wno-ignored-attributes",
501+
]
502+
target = _COFF(host, args=args, prefix="_")
540503
elif re.fullmatch(r"x86_64-apple-darwin.*", host):
541-
target = _MachO(host, ghccc=True, prefix="_")
504+
target = _MachO(host, prefix="_")
542505
elif re.fullmatch(r"x86_64-pc-windows-msvc", host):
543506
args = ["-fms-runtime-lib=dll"]
544-
target = _COFF(host, args=args, ghccc=True)
507+
target = _COFF(host, args=args)
545508
elif re.fullmatch(r"x86_64-.*-linux-gnu", host):
546509
args = ["-fpic"]
547-
target = _ELF(host, args=args, ghccc=True)
510+
target = _ELF(host, args=args)
548511
else:
549512
raise ValueError(host)
550513
return target

Tools/jit/_writer.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ def _dump_stencil(opname: str, group: _stencils.StencilGroup) -> typing.Iterator
6565
if skip:
6666
skip = False
6767
continue
68-
if pair and (folded := hole.fold(pair)):
68+
if pair and (folded := hole.fold(pair, stencil.body)):
6969
skip = True
7070
hole = folded
7171
yield f" {hole.as_c(part)}"

Tools/jit/jit.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
// To use preserve_none in JIT builds, we need to declare a separate function
2+
// pointer with __attribute__((preserve_none)), since this attribute may not be
3+
// supported by the compiler used to build the rest of the interpreter.
4+
typedef jit_func __attribute__((preserve_none)) jit_func_preserve_none;

Tools/jit/template.c

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@
2121

2222
#include "ceval_macros.h"
2323

24+
#include "jit.h"
25+
2426
#undef CURRENT_OPARG
2527
#define CURRENT_OPARG() (_oparg)
2628

@@ -49,7 +51,7 @@
4951
do { \
5052
OPT_STAT_INC(traces_executed); \
5153
__attribute__((musttail)) \
52-
return ((jit_func)((EXECUTOR)->jit_side_entry))(frame, stack_pointer, tstate); \
54+
return ((jit_func_preserve_none)((EXECUTOR)->jit_side_entry))(frame, stack_pointer, tstate); \
5355
} while (0)
5456

5557
#undef GOTO_TIER_ONE
@@ -72,7 +74,7 @@ do { \
7274
do { \
7375
PyAPI_DATA(void) ALIAS; \
7476
__attribute__((musttail)) \
75-
return ((jit_func)&ALIAS)(frame, stack_pointer, tstate); \
77+
return ((jit_func_preserve_none)&ALIAS)(frame, stack_pointer, tstate); \
7678
} while (0)
7779

7880
#undef JUMP_TO_JUMP_TARGET
@@ -86,7 +88,7 @@ do { \
8688

8789
#define TIER_TWO 2
8890

89-
_Py_CODEUNIT *
91+
__attribute__((preserve_none)) _Py_CODEUNIT *
9092
_JIT_ENTRY(_PyInterpreterFrame *frame, _PyStackRef *stack_pointer, PyThreadState *tstate)
9193
{
9294
// Locals that the instruction implementations expect to exist:

Tools/jit/trampoline.c

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,10 @@
44
#include "pycore_frame.h"
55
#include "pycore_jit.h"
66

7-
// This is where the calling convention changes, on platforms that require it.
8-
// The actual change is patched in while the JIT compiler is being built, in
9-
// Tools/jit/_targets.py. On other platforms, this function compiles to nothing.
7+
#include "jit.h"
8+
109
_Py_CODEUNIT *
11-
_ENTRY(_PyInterpreterFrame *frame, _PyStackRef *stack_pointer, PyThreadState *tstate)
10+
_JIT_ENTRY(_PyInterpreterFrame *frame, _PyStackRef *stack_pointer, PyThreadState *tstate)
1211
{
1312
// This is subtle. The actual trace will return to us once it exits, so we
1413
// need to make sure that we stay alive until then. If our trace side-exits
@@ -19,7 +18,7 @@ _ENTRY(_PyInterpreterFrame *frame, _PyStackRef *stack_pointer, PyThreadState *ts
1918
Py_INCREF(executor);
2019
// Note that this is *not* a tail call:
2120
PyAPI_DATA(void) _JIT_CONTINUE;
22-
_Py_CODEUNIT *target = ((jit_func)&_JIT_CONTINUE)(frame, stack_pointer, tstate);
21+
_Py_CODEUNIT *target = ((jit_func_preserve_none)&_JIT_CONTINUE)(frame, stack_pointer, tstate);
2322
Py_SETREF(tstate->previous_executor, executor);
2423
return target;
2524
}

0 commit comments

Comments
 (0)