With the recent release of PyGObject I decided to take a look into something I’ve been meaning to do for some time, using the excellent LLVM library to make things go faster.
But first some brief history. Back in the old PyGTK and pygtk-codegen days all functions were created at compile time generating all the Python<->C wrappers that you would ever need. This is problematic for various reasons:
- each binding needs to be generated before using it and thus you need a Python extension module for each library you use.
- the code generator generated straight-forward wrappers. Not a problem for a small library, but Gtk has almost 200 classes and more than 3000 functions/methods which all need to be registered when you import gtk, which takes time and memory.
GObject-introspection and PyGI solves both of these problems. Typelibs are created in the upstream library and the classes and functions wrappers are created in runtime when you use them. One important thing changed though. Invokation of the native functions are no longer done through generated code, we’re using libffi to do function invokation completely in runtime. This works pretty well but it’s much slower than the old generated functions.
This is where LLVM enters the picture. LLVM is a compiler infrastructure framework. It’s basically a full C/C++ compiler/assembler/linker split into reusable libraries. I decided to take a look into using LLVM to optimize the function invokation by generate native code for the functions lazily using the the LLVM JIT.
I can now announce that after experimenting with it I’ve been able to get it to a point where it’s usable enough to run benchmarks on. That doesn’t mean it’s feature complete, as only a few types such as double,int32,int64,object, so it’s far from supporting a complete application. Anyway, let me present some numbers. First function,
- test_uint64: 19.9x faster
- g_random_double_range(): 20.5x faster
- gi_marshalling_tests_object_new(): 1.01x faster (1.5%)
The first two ares an extreme examples as the functions does very little. But it’s great as it will *only* exercise the time it takes to convert an argument to/from native types.
The last function is not speed up considerably because the time spend most of its time inside the function itself, instantiating GObjects is far more computational expensive than converting Python wrappers back and forth.
Hopefully if this works out, it will be moved into gobject-introspection and not live in pygobject.
Luke: No, it’ll stay in pygobject as it is python specific. It will be possible to write similar code for gjs, but it will be spidermonkey specific etc.
Are these figures relative to plain pygobject or to pygtk?
Cool, how does it compare to the old bindings ?
Interesting,
Where do you think the biggest bottleneck is? the ffi call or building the type conversions before and after the call.
Obviously LLVM solves both for this.. but might be easier for other languages to just cache the conversion data so it does not need looking up each time.
Why not make libffi use llvm instead, so that other libffi users can benefit, too?
Marius: relative to the gi module of pygobject
Stu: I haven’t tested yet, but the code generated is slightly more efficient than the code in the old bindings. The reason I haven’t tested is that the llvm part is far from being able to run
a real app.
Alan: the ffi call in itself is fast, almost all time is spent finding out which type the argument by accessing the typelib information. We don’t access the typelib from the llvm generated code.
glandium: see my answer to Luke & Alan