-
Notifications
You must be signed in to change notification settings - Fork 3.8k
/
aot-compiler.txt
393 lines (297 loc) · 15.6 KB
/
aot-compiler.txt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
Mono Ahead Of Time Compiler
===========================
The Ahead of Time compilation feature in Mono allows Mono to
precompile assemblies to minimize JIT time, reduce memory
usage at runtime and increase the code sharing across multiple
running Mono application.
To precompile an assembly use the following command:
mono --aot -O=all assembly.exe
The `--aot' flag instructs Mono to ahead-of-time compile your
assembly, while the -O=all flag instructs Mono to use all the
available optimizations.
* Caching metadata
------------------
Besides code, the AOT file also contains cached metadata information which allows
the runtime to avoid certain computations at runtime, like the computation of
generic vtables. This reduces both startup time, and memory usage. It is possible
to create an AOT image which contains only this cached information and no code by
using the 'metadata-only' option during compilation:
mono --aot=metadata-only assembly.exe
This works even on platforms where AOT is not normally supported.
* Position Independent Code
---------------------------
On x86 and x86-64 the code generated by Ahead-of-Time compiled
images is position-independent code. This allows the same
precompiled image to be reused across multiple applications
without having different copies: this is the same way in which
ELF shared libraries work: the code produced can be relocated
to any address.
The implementation of Position Independent Code had a
performance impact on Ahead-of-Time compiled images but
compiler bootstraps are still faster than JIT-compiled images,
specially with all the new optimizations provided by the Mono
engine.
* How to support Position Independent Code in new Mono Ports
------------------------------------------------------------
Generated native code needs to reference various runtime
structures/functions whose address is only known at run
time. JITted code can simple embed the address into the native
code, but AOT code needs to do an indirection. This
indirection is done through a table called the Global Offset
Table (GOT), which is similar to the GOT table in the Elf
spec. When the runtime saves the AOT image, it saves some
information for each method describing the GOT table entries
used by that method. When loading a method from an AOT image,
the runtime will fill out the GOT entries needed by the
method.
* Computing the address of the GOT
Methods which need to access the GOT first need to compute its
address. On the x86 it is done by code like this:
call <IP + 5>
pop ebx
add <OFFSET TO GOT>, ebx
<save got addr to a register>
The variable representing the got is stored in
cfg->got_var. It is allways allocated to a global register to
prevent some problems with branches + basic blocks.
* Referencing GOT entries
Any time the native code needs to access some other runtime
structure/function (i.e. any time the backend calls
mono_add_patch_info ()), the code pointed by the patch needs
to load the value from the got. For example, instead of:
call <ABSOLUTE ADDR>
it needs to do:
call *<OFFSET>(<GOT REG>)
Here, the <OFFSET> can be 0, it will be fixed up by the AOT compiler.
For more examples on the changes required, see
svn diff -r 37739:38213 mini-x86.c
* The Program Linkage Table
As in ELF, calls made from AOT code do not go through the GOT. Instead, a direct call is
made to an entry in the Program Linkage Table (PLT). This is based on the fact that on
most architectures, call instructions use a displacement instead of an absolute address, so
they are already position independent. An PLT entry is usually a jump instruction, which
initially points to some trampoline code which transfers control to the AOT loader, which
will compile the called method, and patch the PLT entry so that further calls are made
directly to the called method.
If the called method is in the same assembly, and does not need initialization (i.e. it
doesn't have GOT slots etc), then the call is made directly, bypassing the PLT.
* Implementation
----------------
** The Precompiled File Format
-----------------------------
We use the native object format of the platform. That way it
is possible to reuse existing tools like objdump and the
dynamic loader. All we need is a working assembler, i.e. we
write out a text file which is then passed to gas (the gnu
assembler) to generate the object file.
The precompiled image is stored in a file next to the original
assembly that is precompiled with the native extension for a shared
library (on Linux its ".so" to the generated file).
For example: basic.exe -> basic.exe.so; corlib.dll -> corlib.dll.so
To avoid symbol lookup overhead and to save space, some things like the
compiled code of the individual methods are not identified by specific symbols
like method_code_1234. Instead, they are stored in one big array and the
offsets inside this array are stored in another array, requiring just two
symbols. The offsets array is usually named 'FOO_offsets', where FOO is the
array the offsets refer to, like 'methods', and 'method_offsets'.
Generating code using an assembler and linker has some disadvantages:
- it requires GNU binutils or an equivalent package to be installed on the
machine running the aot compilation.
- it is slow.
There is some support in the aot compiler for directly emitting elf files, but
its not complete (yet).
The following things are saved in the object file and can be
looked up using the equivalent to dlsym:
mono_assembly_guid
A copy of the assembly GUID.
mono_aot_version
The format of the AOT file format.
mono_aot_opt_flags
The optimizations flags used to build this
precompiled image.
method_infos
Contains additional information needed by the runtime for using the
precompiled method, like the GOT entries it uses.
method_info_offsets
Maps method indexes to offsets in the method_infos array.
mono_icall_table
A table that lists all the internal calls
references by the precompiled image.
mono_image_table
A list of assemblies referenced by this AOT
module.
methods
The precompiled code itself.
method_offsets
Maps method indexes to offsets in the methods array.
ex_info
Contains information about methods which is rarely used during normal execution,
like exception and debug info.
ex_info_offsets
Maps method indexes to offsets in the ex_info array.
class_info
Contains precomputed metadata used to speed up various runtime functions.
class_info_offsets
Maps class indexes to offsets in the class_info array.
class_name_table
A hash table mapping class names to class indexes. Used to speed up
mono_class_from_name ().
plt
The Program Linkage Table
plt_info
Contains information needed to find the method belonging to a given PLT entry.
** Source file structure
-----------------------------
The AOT infrastructure is split into two files, aot-compiler.c and
aot-runtime.c. aot-compiler.c contains the AOT compiler which is invoked by
--aot, while aot-runtime.c contains the runtime support needed for loading
code and other things from the aot files.
** Compilation process
----------------------------
AOT compilation consists of the following stages:
- collecting the methods to be compiled.
- compiling them using the JIT.
- emitting the JITted code and other information into an assembly file (.s).
- assembling the file using the system assembler.
- linking the resulting object file into a shared library using the system
linker.
** Handling compiled code
----------------------------
Each method is identified by a method index. For normal methods, this is
equivalent to its index in the METHOD metadata table. For runtime generated
methods (wrappers), it is an arbitrary number.
Compiled code is created by invoking the JIT, requesting it to created AOT
code instead of normal code. This is done by the compile_method () function.
The output of the JIT is compiled code and a set of patches (relocations). Each
relocation specifies an offset inside the compiled code, and a runtime object
whose address is accessed at that offset.
Patches are described by a MonoJumpInfo structure. From the perspective
of the AOT compiler, there are two kinds of patches:
- calls, which require an entry in the PLT table.
- everything else, which require an entry in the GOT table.
How patches is handled is described in the next section.
After all the method are compiled, they are emitted into the output file into
a byte array called 'methods', The emission
is done by the emit_method_code () and emit_and_reloc_code () functions. Each
piece of compiled code is identified by the local symbol .Lm_<method index>.
While compiled code is emitted, all the locations which have an associated patch
are rewritten using a platform specific process so the final generated code will
refer to the plt and got entries belonging to the patches.
The compiled code array
can be accessed using the 'methods' global symbol.
** Handling patches
----------------------------
Before a piece of AOTed code can be used, the GOT entries used by it must be
filled out with the addresses of runtime objects. Those objects are identified
by MonoJumpInfo structures. These stuctures are saved in a serialized form in
the AOT file, so the AOT loader can deconstruct them. The serialization is done
by the encode_patch () function, while the deserialization is done by the
decode_patch_info () function.
Every method has an associated method info blob inside the 'method_info' byte
array in the AOT file. This contains all the information required to load the
method at runtime:
- the first got entry used by the method.
- the number of got entries used by the method.
- the serialized patch info for the got entries.
Some patches, like vtables, icalls are very common, so instead of emitting their
info every time they are used by a method, we emit the info only once into a
byte array named 'got_info', and only emit an index into this array for every
access.
** The Procedure Linkage Table (PLT)
------------------------------------
Our PLT is similar to the elf PLT, it is used to handle calls between methods.
If method A needs to call method B, then an entry is allocated in the PLT for
method B, and A calls that entry instead of B directly. This is useful because
in some cases the runtime needs to do some processing the first time B is
called.
There are two cases:
- if B is in another assembly, then it needs to be looked up, then JITted or the
corresponding AOT code needs to be found.
- if B is in the same assembly, but has got slots, then the got slots need to be
initialized.
If none of these cases is true, then the PLT is not used, and the call is made
directly to the native code of the target method.
A PLT entry is usually implemented by a jump though a jump table, where the
jump table entries are initially filled up with the address of a trampoline so
the runtime can get control, and after the native code of the called method is
created/found, the jump table entry is changed to point to the native code.
All PLT entries also embed a integer offset after the jump which indexes into
the 'plt_info' table, which stores the information required to find the called
method. The PLT is emitted by the emit_plt () function.
** Exception/Debug info
----------------------------
Each compiled method has some additional info generated by the JIT, usable
for debugging (IL offset-native offset maps) and exception handling
(saved registers, native offsets of try/catch clauses). Since this info is
rarely needed, it is saved into a separate byte array called 'ex_info'.
** Cached metadata
---------------------------
When the runtime loads a class, it needs to compute a variety of information
which is not readily available in the metadata, like the instance size,
vtable, whenever the class has a finalizer/type initializer etc. Computing this
information requires a lot of time, causes the loading of lots of metadata,
and it usually involves the creation of many runtime data structures
(MonoMethod/MonoMethodSignature etc), which are long living, and usually persist
for the lifetime of the app. To avoid this, we compute the required information
at aot compilation time, and save it into the aot image, into an array called
'class_info'. The runtime can query this information using the
mono_aot_get_cached_class_info () function, and if the information is available,
it can avoid computing it.
** Full AOT mode
-------------------------
Some platforms like the iphone prohibit JITted code, using technical and/or
legal means. This is a significant problem for the mono runtime, since it
generates a lot of code dynamically, using either the JIT or more low-level
code generation macros. To solve this, the AOT compiler is able to function in
full-aot or aot-only mode, where it generates and saves all the neccesary code
in the aot image, so at runtime, no code needs to be generated.
There are two kinds of code which needs to be considered:
- wrapper methods, that is methods whose IL is generated dynamically by the
runtime. They are handled by generating them in the add_wrappers () function,
then emitting them the same way as the 'normal' methods. The only problem is
that these methods do not have a methoddef token, so we need a separate table
in the aot image ('wrapper_info') to find their method index.
- trampolines and other small hand generated pieces of code. They are handled
in an ad-hoc way in the emit_trampolines () function.
* Performance considerations
----------------------------
Using AOT code is a trade-off which might lead to higher or
slower performance, depending on a lot of circumstances. Some
of these are:
- AOT code needs to be loaded from disk before being used, so
cold startup of an application using AOT code MIGHT be
slower than using JITed code. Warm startup (when the code is
already in the machines cache) should be faster. Also,
JITing code takes time, and the JIT compiler also need to
load additional metadata for the method from the disk, so
startup can be faster even in the cold startup case.
- AOT code is usually compiled with all optimizations turned
on, while JITted code is usually compiled with default
optimizations, so the generated code in the AOT case should
be faster.
- JITted code can directly access runtime data structures and
helper functions, while AOT code needs to go through an
indirection (the GOT) to access them, so it will be slower
and somewhat bigger as well.
- When JITting code, the JIT compiler needs to load a lot of
metadata about methods and types into memory.
- JITted code has better locality, meaning that if A method
calls B, then the native code for A and B is usually quite
close in memory, leading to better cache behaviour thus
improved performance. In contrast, the native code of
methods inside the AOT file is in a somewhat random order.
* Future Work
-------------
- Currently, when an AOT module is loaded, all of its
dependent assemblies are also loaded eagerly, and these
assemblies need to be exactly the same as the ones loaded
when the AOT module was created ('hard binding'). Non-hard
binding should be allowed.
- On x86, the generated code uses call 0, pop REG, add
GOTOFFSET, REG to materialize the GOT address. Newer
versions of gcc use a separate function to do this, maybe we
need to do the same.
- Currently, we get vtable addresses from the GOT. Another
solution would be to store the data from the vtables in the
.bss section, so accessing them would involve less
indirection.