Skip to content

Commit ddfa738

Browse files
committed
Encapsulate 'CompilerCache' entirely within NativeCompiler.
1 parent 328457b commit ddfa738

File tree

5 files changed

+169
-108
lines changed

5 files changed

+169
-108
lines changed
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
# Copyright 2023 typed_python Authors
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
import llvmlite.binding as llvm
16+
import llvmlite.ir
17+
import typed_python.compiler.native_compiler.native_ast_to_llvm as native_ast_to_llvm
18+
19+
import ctypes
20+
from typed_python import _types
21+
22+
llvm.initialize()
23+
llvm.initialize_native_target()
24+
llvm.initialize_native_asmprinter() # yes, even this one
25+
26+
target_triple = llvm.get_process_triple()
27+
target = llvm.Target.from_triple(target_triple)
28+
target_machine = target.create_target_machine()
29+
target_machine_shared_object = target.create_target_machine(reloc='pic', codemodel='default')
30+
31+
ctypes.CDLL(_types.__file__, mode=ctypes.RTLD_GLOBAL)
32+
33+
34+
pointer_size = (
35+
llvmlite.ir.PointerType(llvmlite.ir.DoubleType())
36+
.get_abi_size(target_machine.target_data)
37+
)
38+
39+
assert pointer_size == native_ast_to_llvm.pointer_size
40+
41+
42+
def sizeof_native_type(native_type):
43+
if native_type.matches.Void:
44+
return 0
45+
46+
return (
47+
native_ast_to_llvm.type_to_llvm_type(native_type)
48+
.get_abi_size(target_machine.target_data)
49+
)
50+
51+
52+
# there can be only one llvm engine alive at once.
53+
_engineCache = []
54+
55+
56+
def create_execution_engine(inlineThreshold):
57+
if _engineCache:
58+
return _engineCache[0]
59+
60+
pmb = llvm.create_pass_manager_builder()
61+
pmb.opt_level = 3
62+
pmb.size_level = 0
63+
pmb.inlining_threshold = inlineThreshold
64+
pmb.loop_vectorize = True
65+
pmb.slp_vectorize = True
66+
67+
pass_manager = llvm.create_module_pass_manager()
68+
pmb.populate(pass_manager)
69+
70+
target_machine.add_analysis_passes(pass_manager)
71+
72+
# And an execution engine with an empty backing module
73+
backing_mod = llvm.parse_assembly("")
74+
engine = llvm.create_mcjit_compiler(backing_mod, target_machine)
75+
76+
_engineCache.append((engine, pass_manager))
77+
78+
return engine, pass_manager

typed_python/compiler/native_compiler/native_compiler.py

Lines changed: 71 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Copyright 2019 typed_python Authors
1+
# Copyright 2023 typed_python Authors
22
#
33
# Licensed under the Apache License, Version 2.0 (the "License");
44
# you may not use this file except in compliance with the License.
@@ -13,75 +13,14 @@
1313
# limitations under the License.
1414

1515
import llvmlite.binding as llvm
16-
import llvmlite.ir
1716
import typed_python.compiler.native_compiler.native_ast as native_ast
1817
import typed_python.compiler.native_compiler.native_ast_to_llvm as native_ast_to_llvm
19-
18+
from typed_python.compiler.native_compiler.compiler_cache import CompilerCache
19+
from typed_python.compiler.native_compiler.llvm_execution_engine import create_execution_engine
2020
from typed_python.compiler.native_compiler.loaded_module import LoadedModule
2121
from typed_python.compiler.native_compiler.native_function_pointer import NativeFunctionPointer
2222
from typed_python.compiler.native_compiler.binary_shared_object import BinarySharedObject
2323

24-
import ctypes
25-
from typed_python import _types
26-
27-
llvm.initialize()
28-
llvm.initialize_native_target()
29-
llvm.initialize_native_asmprinter() # yes, even this one
30-
31-
target_triple = llvm.get_process_triple()
32-
target = llvm.Target.from_triple(target_triple)
33-
target_machine = target.create_target_machine()
34-
target_machine_shared_object = target.create_target_machine(reloc='pic', codemodel='default')
35-
36-
ctypes.CDLL(_types.__file__, mode=ctypes.RTLD_GLOBAL)
37-
38-
39-
pointer_size = (
40-
llvmlite.ir.PointerType(llvmlite.ir.DoubleType())
41-
.get_abi_size(target_machine.target_data)
42-
)
43-
44-
assert pointer_size == native_ast_to_llvm.pointer_size
45-
46-
47-
def sizeof_native_type(native_type):
48-
if native_type.matches.Void:
49-
return 0
50-
51-
return (
52-
native_ast_to_llvm.type_to_llvm_type(native_type)
53-
.get_abi_size(target_machine.target_data)
54-
)
55-
56-
57-
# there can be only one llvm engine alive at once.
58-
_engineCache = []
59-
60-
61-
def create_execution_engine(inlineThreshold):
62-
if _engineCache:
63-
return _engineCache[0]
64-
65-
pmb = llvm.create_pass_manager_builder()
66-
pmb.opt_level = 3
67-
pmb.size_level = 0
68-
pmb.inlining_threshold = inlineThreshold
69-
pmb.loop_vectorize = True
70-
pmb.slp_vectorize = True
71-
72-
pass_manager = llvm.create_module_pass_manager()
73-
pmb.populate(pass_manager)
74-
75-
target_machine.add_analysis_passes(pass_manager)
76-
77-
# And an execution engine with an empty backing module
78-
backing_mod = llvm.parse_assembly("")
79-
engine = llvm.create_mcjit_compiler(backing_mod, target_machine)
80-
81-
_engineCache.append((engine, pass_manager))
82-
83-
return engine, pass_manager
84-
8524

8625
class NativeCompiler:
8726
""""Engine for compiling bundles of native_ast.Function objects into NativeFunctionPointers.
@@ -91,15 +30,23 @@ class NativeCompiler:
9130
* compiling functions into a runnable form using llvm
9231
* performing any runtime-based performance optimizations
9332
* maintaining the compiler cache
33+
34+
Note that this class is NOT threadsafe and clients are expected to serialize their
35+
access through Runtime.
9436
"""
9537
def __init__(self, inlineThreshold):
38+
self.compilerCache = None
9639
self.engine, self.module_pass_manager = create_execution_engine(inlineThreshold)
9740
self.converter = native_ast_to_llvm.Converter()
9841
self.functions_by_name = {}
9942
self.inlineThreshold = inlineThreshold
10043
self.verbose = False
10144
self.optimize = True
10245

46+
def initializeCompilerCache(self, compilerCacheDir):
47+
"""Indicate that we should use a compiler cache from disk at 'compilerCacheDir'."""
48+
self.compilerCache = CompilerCache(compilerCacheDir)
49+
10350
def markExternal(self, functionNameToType):
10451
"""Provide type signatures for a set of external functions."""
10552
self.converter.markExternal(functionNameToType)
@@ -110,7 +57,65 @@ def mark_converter_verbose(self):
11057
def mark_llvm_codegen_verbose(self):
11158
self.verbose = True
11259

113-
def buildSharedObject(self, functions):
60+
def addFunctions(
61+
self,
62+
# map from str to native_ast.Function
63+
functionDefinitions,
64+
# map from str to the TypedCallTarget for any function that's actually typed
65+
typedCallTargets,
66+
externallyUsed
67+
):
68+
"""Add a collection of functions to the compiler.
69+
70+
Once a function has been added, we can request a NativeFunctionPointer for it.
71+
"""
72+
if self.compilerCache is None:
73+
loadedModule = self._buildModule(functionDefinitions)
74+
loadedModule.linkGlobalVariables()
75+
else:
76+
binary = self._buildSharedObject(functionDefinitions)
77+
78+
self.compilerCache.addModule(
79+
binary,
80+
typedCallTargets,
81+
externallyUsed
82+
)
83+
84+
def functionPointerByName(self, linkerName) -> NativeFunctionPointer:
85+
"""Find a NativeFunctionPointer for a given link-time name.
86+
87+
Args:
88+
linkerName (str) - the name of the compiled symbol we want.
89+
90+
Returns:
91+
a NativeFunctionPointer or None if the function has never been defined.
92+
"""
93+
if self.compilerCache is not None:
94+
# the compiler cache has every shared object and can load them
95+
return self.compilerCache.function_pointer_by_name(linkerName)
96+
97+
# the llvm compiler is just building shared objects, but the
98+
# compiler cache has all the pointers.
99+
return self.functions_by_name.get(linkerName)
100+
101+
def loadFromCache(self, linkName):
102+
"""Attempt to load a cached copy of 'linkName' and all reachable code.
103+
104+
If it isn't defined, or has already been defined, return None. If we're loading it
105+
for the first time, return a pair
106+
107+
(typedCallTargets, nativeTypes)
108+
109+
where typedCallTargets is a map from linkName to TypedCallTarget, and nativeTypes is
110+
a map from linkName to native_ast.Type.Function giving the native implementation type.
111+
112+
WARNING: this will return None if you already called 'functionPointerByName' on it
113+
"""
114+
if self.compilerCache:
115+
if self.compilerCache.hasSymbol(linkName):
116+
return self.compilerCache.loadForSymbol(linkName)
117+
118+
def _buildSharedObject(self, functions):
114119
"""Add native definitions and return a BinarySharedObject representing the compiled code."""
115120
module = self.converter.add_functions(functions)
116121

@@ -135,10 +140,7 @@ def buildSharedObject(self, functions):
135140
module.functionNameToType,
136141
)
137142

138-
def function_pointer_by_name(self, name):
139-
return self.functions_by_name.get(name)
140-
141-
def buildModule(self, functions):
143+
def _buildModule(self, functions):
142144
"""Compile a list of functions into a new module.
143145
144146
Args:

typed_python/compiler/native_compiler/native_compiler_test.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ def test_global_variable_pointers():
5757
# is global.
5858
nativeCompiler = Runtime.singleton().native_compiler
5959

60-
loadedModule = nativeCompiler.buildModule(
60+
loadedModule = nativeCompiler._buildModule(
6161
dict(
6262
_readGlobalVarFunc=readGlobalVarFunc,
6363
_returnGlobalVarPtrFunc=returnGlobalVarPtrFunc
@@ -111,7 +111,7 @@ def test_create_binary_shared_object():
111111

112112
nativeCompiler = Runtime.singleton().native_compiler
113113

114-
bso = nativeCompiler.buildSharedObject(
114+
bso = nativeCompiler._buildSharedObject(
115115
{'__test_f_2': f}
116116
)
117117

typed_python/compiler/python_to_native_converter.py

Lines changed: 12 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -41,11 +41,8 @@
4141

4242

4343
class PythonToNativeConverter:
44-
def __init__(self, llvmCompiler, compilerCache):
45-
object.__init__(self)
46-
47-
self.llvmCompiler = llvmCompiler
48-
self.compilerCache = compilerCache
44+
def __init__(self, nativeCompiler):
45+
self.nativeCompiler = nativeCompiler
4946

5047
# if True, then insert additional code to check for undefined behavior.
5148
self.generateDebugChecks = False
@@ -116,11 +113,6 @@ def buildAndLinkNewModule(self):
116113
if not targets:
117114
return
118115

119-
if self.compilerCache is None:
120-
loadedModule = self.llvmCompiler.buildModule(targets)
121-
loadedModule.linkGlobalVariables()
122-
return
123-
124116
# get a set of function names that we depend on
125117
externallyUsed = set()
126118

@@ -132,10 +124,8 @@ def buildAndLinkNewModule(self):
132124
if depLN not in targets:
133125
externallyUsed.add(depLN)
134126

135-
binary = self.llvmCompiler.buildSharedObject(targets)
136-
137-
self.compilerCache.addModule(
138-
binary,
127+
self.nativeCompiler.addFunctions(
128+
targets,
139129
{name: self._targets[name] for name in targets if name in self._targets},
140130
externallyUsed
141131
)
@@ -209,18 +199,16 @@ def defineLinkName(self, identity, linkName):
209199
return True
210200

211201
def _loadFromCompilerCache(self, linkName):
212-
if self.compilerCache:
213-
if self.compilerCache.hasSymbol(linkName):
214-
callTargetsAndTypes = self.compilerCache.loadForSymbol(linkName)
202+
callTargetsAndTypes = self.nativeCompiler.loadFromCache(linkName)
215203

216-
if callTargetsAndTypes is not None:
217-
newTypedCallTargets, newNativeFunctionTypes = callTargetsAndTypes
204+
if callTargetsAndTypes is not None:
205+
newTypedCallTargets, newNativeFunctionTypes = callTargetsAndTypes
218206

219-
self._targets.update(newTypedCallTargets)
220-
self.llvmCompiler.markExternal(newNativeFunctionTypes)
207+
self._targets.update(newTypedCallTargets)
208+
self.nativeCompiler.markExternal(newNativeFunctionTypes)
221209

222-
self._allDefinedNames.update(newNativeFunctionTypes)
223-
self._allCachedNames.update(newNativeFunctionTypes)
210+
self._allDefinedNames.update(newNativeFunctionTypes)
211+
self._allCachedNames.update(newNativeFunctionTypes)
224212

225213
def defineNonPythonFunction(self, name, identityTuple, context):
226214
"""Define a non-python generating function (if we haven't defined it before already)
@@ -604,13 +592,7 @@ def functionPointerByName(self, linkerName) -> NativeFunctionPointer:
604592
Returns:
605593
a NativeFunctionPointer or None
606594
"""
607-
if self.compilerCache is None:
608-
# the llvm compiler holds it all
609-
return self.llvmCompiler.function_pointer_by_name(linkerName)
610-
else:
611-
# the llvm compiler is just building shared objects, but the
612-
# compiler cache has all the pointers.
613-
return self.compilerCache.function_pointer_by_name(linkerName)
595+
return self.nativeCompiler.functionPointerByName(linkerName)
614596

615597
def convertTypedFunctionCall(self, functionType, overloadIx, inputWrappers, assertIsRoot=False):
616598
overload = functionType.overloads[overloadIx]

typed_python/compiler/runtime.py

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@
2121
import typed_python
2222
from typed_python.compiler.runtime_lock import runtimeLock
2323
from typed_python.compiler.conversion_level import ConversionLevel
24-
from typed_python.compiler.native_compiler.compiler_cache import CompilerCache
2524
from typed_python.type_function import TypeFunction
2625
from typed_python.compiler.type_wrappers.typed_tuple_masquerading_as_tuple_wrapper import TypedTupleMasqueradingAsTuple
2726
from typed_python.compiler.type_wrappers.named_tuple_masquerading_as_dict_wrapper import NamedTupleMasqueradingAsDict
@@ -201,16 +200,16 @@ def singleton():
201200
return _singleton[0]
202201

203202
def __init__(self):
203+
self.native_compiler = native_compiler.NativeCompiler(
204+
inlineThreshold=100
205+
)
204206
if os.getenv("TP_COMPILER_CACHE"):
205-
self.compilerCache = CompilerCache(
207+
self.native_compiler.initializeCompilerCache(
206208
os.path.abspath(os.getenv("TP_COMPILER_CACHE"))
207209
)
208-
else:
209-
self.compilerCache = None
210-
self.native_compiler = native_compiler.NativeCompiler(inlineThreshold=100)
210+
211211
self.converter = python_to_native_converter.PythonToNativeConverter(
212-
self.native_compiler,
213-
self.compilerCache
212+
self.native_compiler
214213
)
215214
self.lock = runtimeLock
216215
self.timesCompiled = 0

0 commit comments

Comments
 (0)