mirror of
https://git.postgresql.org/git/postgresql.git
synced 2024-09-11 23:17:46 +02:00
Most of these are new, as of
|
||
---|---|---|
.. | ||
llvm | ||
jit.c | ||
Makefile | ||
README |
What is Just-in-Time Compilation? ================================= Just-in-Time compilation (JIT) is the process of turning some form of interpreted program evaluation into a native program, and doing so at runtime. For example, instead of using a facility that can evaluate arbitrary SQL expressions to evaluate an SQL predicate like WHERE a.col = 3, it is possible to generate a function than can be natively executed by the CPU that just handles that expression, yielding a speedup. This is JIT, rather than ahead-of-time (AOT) compilation, because it is done at query execution time, and perhaps only in cases where the relevant task is repeated a number of times. Given the way JIT compilation is used in PostgreSQL, the lines between interpretation, AOT and JIT are somewhat blurry. Note that the interpreted program turned into a native program does not necessarily have to be a program in the classical sense. E.g. it is highly beneficial to JIT compile tuple deforming into a native function just handling a specific type of table, despite tuple deforming not commonly being understood as a "program". Why JIT? ======== Parts of PostgreSQL are commonly bottlenecked by comparatively small pieces of CPU intensive code. In a number of cases that is because the relevant code has to be very generic (e.g. handling arbitrary SQL level expressions, over arbitrary tables, with arbitrary extensions installed). This often leads to a large number of indirect jumps and unpredictable branches, and generally a high number of instructions for a given task. E.g. just evaluating an expression comparing a column in a database to an integer ends up needing several hundred cycles. By generating native code large numbers of indirect jumps can be removed by either making them into direct branches (e.g. replacing the indirect call to an SQL operator's implementation with a direct call to that function), or by removing it entirely (e.g. by evaluating the branch at compile time because the input is constant). Similarly a lot of branches can be entirely removed (e.g. by again evaluating the branch at compile time because the input is constant). The latter is particularly beneficial for removing branches during tuple deforming. How to JIT ========== PostgreSQL, by default, uses LLVM to perform JIT. LLVM was chosen because it is developed by several large corporations and therefore unlikely to be discontinued, because it has a license compatible with PostgreSQL, and because its IR can be generated from C using the Clang compiler. Shared Library Separation ------------------------- To avoid the main PostgreSQL binary directly depending on LLVM, which would prevent LLVM support being independently installed by OS package managers, the LLVM dependent code is located in a shared library that is loaded on-demand. An additional benefit of doing so is that it is relatively easy to evaluate JIT compilation that does not use LLVM, by changing out the shared library used to provide JIT compilation. To achieve this, code intending to perform JIT (e.g. expression evaluation) calls an LLVM independent wrapper located in jit.c to do so. If the shared library providing JIT support can be loaded (i.e. PostgreSQL was compiled with LLVM support and the shared library is installed), the task of JIT compiling an expression gets handed off to the shared library. This obviously requires that the function in jit.c is allowed to fail in case no JIT provider can be loaded. Which shared library is loaded is determined by the jit_provider GUC, defaulting to "llvmjit". Cloistering code performing JIT into a shared library unfortunately also means that code doing JIT compilation for various parts of code has to be located separately from the code doing so without JIT. E.g. the JIT version of execExprInterp.c is located in jit/llvm/ rather than executor/. JIT Context ----------- For performance and convenience reasons it is useful to allow JITed functions to be emitted and deallocated together. It is e.g. very common to create a number of functions at query initialization time, use them during query execution, and then deallocate all of them together at the end of the query. Lifetimes of JITed functions are managed via JITContext. Exactly one such context should be created for work in which all created JITed function should have the same lifetime. E.g. there's exactly one JITContext for each query executed, in the query's EState. Only the release of a JITContext is exposed to the provider independent facility, as the creation of one is done on-demand by the JIT implementations. Emitting individual functions separately is more expensive than emitting several functions at once, and emitting them together can provide additional optimization opportunities. To facilitate that, the LLVM provider separates defining functions from optimizing and emitting functions in an executable manner. Creating functions into the current mutable module (a module essentially is LLVM's equivalent of a translation unit in C) is done using extern LLVMModuleRef llvm_mutable_module(LLVMJitContext *context); in which it then can emit as much code using the LLVM APIs as it wants. Whenever a function actually needs to be called extern void *llvm_get_function(LLVMJitContext *context, const char *funcname); returns a pointer to it. E.g. in the expression evaluation case this setup allows most functions in a query to be emitted during ExecInitNode(), delaying the function emission to the time the first time a function is actually used. Error Handling -------------- There are two aspects of error handling. Firstly, generated (LLVM IR) and emitted functions (mmap()ed segments) need to be cleaned up both after a successful query execution and after an error. This is done by registering each created JITContext with the current resource owner, and cleaning it up on error / end of transaction. If it is desirable to release resources earlier, jit_release_context() can be used. The second, less pretty, aspect of error handling is OOM handling inside LLVM itself. The above resowner based mechanism takes care of cleaning up emitted code upon ERROR, but there's also the chance that LLVM itself runs out of memory. LLVM by default does *not* use any C++ exceptions. Its allocations are primarily funneled through the standard "new" handlers, and some direct use of malloc() and mmap(). For the former a 'new handler' exists: http://en.cppreference.com/w/cpp/memory/new/set_new_handler For the latter LLVM provides callbacks that get called upon failure (unfortunately mmap() failures are treated as fatal rather than OOM errors). What we've chosen to do for now is have two functions that LLVM using code must use: extern void llvm_enter_fatal_on_oom(void); extern void llvm_leave_fatal_on_oom(void); before interacting with LLVM code. When a libstdc++ new or LLVM error occurs, the handlers set up by the above functions trigger a FATAL error. We have to use FATAL rather than ERROR, as we *cannot* reliably throw ERROR inside a foreign library without risking corrupting its internal state. Users of the above sections do *not* have to use PG_TRY/CATCH blocks, the handlers instead are reset on toplevel sigsetjmp() level. Using a relatively small enter/leave protected section of code, rather than setting up these handlers globally, avoids negative interactions with extensions that might use C++ such as PostGIS. As LLVM code generation should never execute arbitrary code, just setting these handlers temporarily ought to suffice. Type Synchronization -------------------- To be able to generate code that can perform tasks done by "interpreted" PostgreSQL, it obviously is required that code generation knows about at least a few PostgreSQL types. While it is possible to inform LLVM about type definitions by recreating them manually in C code, that is failure prone and labor intensive. Instead there is one small file (llvmjit_types.c) which references each of the types required for JITing. That file is translated to bitcode at compile time, and loaded when LLVM is initialized in a backend. That works very well to synchronize the type definition, but unfortunately it does *not* synchronize offsets as the IR level representation doesn't know field names. Instead, required offsets are maintained as defines in the original struct definition, like so: #define FIELDNO_TUPLETABLESLOT_NVALID 9 int tts_nvalid; /* # of valid values in tts_values */ While that still needs to be defined, it's only required for a relatively small number of fields, and it's bunched together with the struct definition, so it's easily kept synchronized. Inlining -------- One big advantage of JITing expressions is that it can significantly reduce the overhead of PostgreSQL's extensible function/operator mechanism, by inlining the body of called functions/operators. It obviously is undesirable to maintain a second implementation of commonly used functions, just for inlining purposes. Instead we take advantage of the fact that the Clang compiler can emit LLVM IR. The ability to do so allows us to get the LLVM IR for all operators (e.g. int8eq, float8pl etc), without maintaining two copies. These bitcode files get installed into the server's $pkglibdir/bitcode/postgres/ Using existing LLVM functionality (for parallel LTO compilation), additionally an index is over these is stored to $pkglibdir/bitcode/postgres.index.bc Similarly extensions can install code into $pkglibdir/bitcode/[extension]/ accompanied by $pkglibdir/bitcode/[extension].index.bc just alongside the actual library. An extension's index will be used to look up symbols when located in the corresponding shared library. Symbols that are used inside the extension, when inlined, will be first looked up in the main binary and then the extension's. Caching ------- Currently it is not yet possible to cache generated functions, even though that'd be desirable from a performance point of view. The problem is that the generated functions commonly contain pointers into per-execution memory. The expression evaluation machinery needs to be redesigned a bit to avoid that. Basically all per-execution memory needs to be referenced as an offset to one block of memory stored in an ExprState, rather than absolute pointers into memory. Once that is addressed, adding an LRU cache that's keyed by the generated LLVM IR will allow the usage of optimized functions even for faster queries. A longer term project is to move expression compilation to the planner stage, allowing e.g. to tie compiled expressions to prepared statements. An even more advanced approach would be to use JIT with few optimizations initially, and build an optimized version in the background. But that's even further off. What to JIT =========== Currently expression evaluation and tuple deforming are JITed. Those were chosen because they commonly are major CPU bottlenecks in analytics queries, but are by no means the only potentially beneficial cases. For JITing to be beneficial a piece of code first and foremost has to be a CPU bottleneck. But also importantly, JITing can only be beneficial if overhead can be removed by doing so. E.g. in the tuple deforming case the knowledge about the number of columns and their types can remove a significant number of branches, and in the expression evaluation case a lot of indirect jumps/calls can be removed. If neither of these is the case, JITing is a waste of resources. Future avenues for JITing are tuple sorting, COPY parsing/output generation, and later compiling larger parts of queries. When to JIT =========== Currently there are a number of GUCs that influence JITing: - jit_above_cost = -1, 0-DBL_MAX - all queries with a higher total cost get JITed, *without* optimization (expensive part), corresponding to -O0. This commonly already results in significant speedups if expression/deforming is a bottleneck (removing dynamic branches mostly). - jit_optimize_above_cost = -1, 0-DBL_MAX - all queries with a higher total cost get JITed, *with* optimization (expensive part). - jit_inline_above_cost = -1, 0-DBL_MAX - inlining is tried if query has higher cost. Whenever a query's total cost is above these limits, JITing is performed. Alternative costing models, e.g. by generating separate paths for parts of a query with lower cpu_* costs, are also a possibility, but it's doubtful the overhead of doing so is sufficient. Another alternative would be to count the number of times individual expressions are estimated to be evaluated, and perform JITing of these individual expressions. The obvious seeming approach of JITing expressions individually after a number of execution turns out not to work too well. Primarily because emitting many small functions individually has significant overhead. Secondarily because the time until JITing occurs causes relative slowdowns that eat into the gain of JIT compilation.