Tales about Aliaksey's life

Wednesday, September 9, 2026

DWARF backtracing can be quick and safe

There is a widespread perception that DWARF-based unwinders are slow and crashy. For example, a famous rant by Linus: https://lkml.org/lkml/2012/2/10/356. That rant applies to one specific Linux sub-project. User space has its own challenges with DWARF-based unwinding. The kernel uses an alternative called ORC. At least two efforts now aim to replace or amend DWARF unwinding in user space. And of course, there is a somewhat popular movement to build everything with frame pointers. But frame pointers don’t make all backtraces perfect. Not for CPU profiling, where profiling “ticks” in epilogues, prologues, or PLT are common. And imperfect backtraces can be confusing and painful to deal with.

We can do much better than that.
So for the last few months I was building aw-backtrace. aw-backtrace is the DWARF-based unwinder that won't crash and is very quick. It is a good-sized step towards perfect backtraces.
While I cannot “mathematically prove” that aw-backtrace cannot crash, I did put a serious effort into performing bounds checking for all the accesses, both for reading “DWARF stuff” and for reading register values from the stack.
Here is a quick introduction to DWARF unwind info. Every compiled function has FDE (slight simplification, but good enough for an intro). Each compile unit (.o file) has a CIE that describes language and architectural defaults (e.g., x86 functions start with the return address pushed on the stack). Another notion in the unwind info is the CFA, which is basically the stack pointer at the call site. Unwind info typically looks like: “to establish CFA take RSP register and add 16, then to take return address read machine word at CFA - 8, value of the RBP register is saved at CFA - 16”. This is represented as a sequence of CFI instructions that define such unwind info for each instruction in the function. Tools like readelf (with the -wf flag) can dump this and show you what it looks like. Alternatively, checking the compiler's assembly output is useful too, e.g., on godbolt: https://godbolt.org/z/dErnbcnT1
What makes DWARF unwinding a slightly harder target is that it was originally produced for debug info and was intentionally very powerful and general. But that means nearly unbounded complexity. Stuff like an arbitrarily deep stack of .cfi_remember_state. In practice, as we know from ORC and related efforts, compilers produce a fairly limited “shape” of unwind info. There are occasional non-trivial DWARF unwind info. For example, some hero has put into glibc for Linux/x86-64 (but I think only there) a very elaborate unwind info for signal trampolines. Because describing the restoration of every register from ucontext’s gregs fields involves indirections, it is full of DWARF expressions. Another very smart example is unwind info for the PLT section produced by the GNU linker (but for some reason the LLVM linker doesn’t produce any unwind info for PLTs).
For aw-backtrace, the explicit choice is to keep things safe. So it supports only a practical subset of unwind info, roughly on par with SFrame V3. aw-backtrace special-cases and explicitly handles known unusual cases like signal handler trampolines or the PLT. There are currently few functions that I see in libc (e.g., longjmp)  and OpenSSL where unwind info is too advanced for aw-backtrace. I plan to address this eventually, but what it has now comfortably covers the vast majority of code.
I believe what compromised DWARF backtracers before is that backtracing was so far always just a side effect of more general code for propagating exceptions. If we’re in the middle of unwinding a stack for an exception and face a function with broken or excessively complex unwind info, there is nowhere to go anyway. With backtracing, you can always stop unwinding. Yes, truncated backtraces are imperfect. But crashing or hanging is much worse.
How do I know that uncommon unwind info is rare? It is straightforward to dump unwind info and grep for DWARF expressions. But also because I single-stepped through a bunch of code while comparing the actual backtrace with what aw-backtrace produces.
It is actually a somewhat remarkable piece. Grown out of my earlier effort to “torture” atfork handling in gperftools. I have code that sets the x86 TF flag, which makes the CPU “single step”. Stepping delivers an interrupt to the kernel, which passes it to user space as SIGTRAP. From that SIGTRAP handler, we run the comparer. To speed this up, I built a “mini interpreter” for the common subset of x86 instructions, so we don’t go through the interrupt/signal boundary every time. There are, of course, a bunch of nasty details, like fighting with other code for altstack or dealing with SIGTRAP masking. So it is by no means perfect. Some details are in the README, and the code is hopefully not too large and somewhat readable. I am actively replacing it with a portable solution that single-steps via a QEMU plugin.
Another part of the DWARF inspection sub-effort is the unwind-check tool I released together with aw-backtrace. It disassembles the code, compares its effects with unwind info, and complains if there is a mismatch. It already found 2 bugs in LLVM (but thankfully the mismatches are relatively short and don’t cross call-sites). In the future, I hope and expect compilers will “never again” produce bad unwind info. Because verifying unwind info is relatively straightforward.
And finally I have things to say about backtracing speed. We will likely never fully match the speed of trivial frame-pointer-based backtracers, but it is relatively easy to come very close. The key is caching. Workloads that capture many backtraces tend to step through the same call sites. Implementing a lockless, async-signal-safe cache was a mild challenge, but ultimately it is just a matter of engineering. The README has some details, and the code should be very readable too (for now, the focus was to keep things mega-simple; I undid the more advanced caching and eviction approach so the code is hopefully easy to verify and understand).
Beyond caching, yes, DWARF unwind info is by no means perfect for performance. Those inconvenient augmentation details. Or having to decode padding NOPs in CIE. But the basic stuff (redundant instructions in CIEs, basic prologue/epilogue bits) is basically very bounded in size and thus in decoding cost. The bigger inefficiency is having to advance sequentially from the start of the function to where we need to unwind.
In most cases, even larger functions are fairly straightforward. Most of such functions are covered by one or two large advances. But sometimes unwind info gets large when smaller advances occur between CFI updates. This happens more easily for functions that pass arguments on the stack, since pushing or popping anything updates frame size; in the common case of using stack pointer-based frame layout, it requires updating unwind info.
aw-backtrace comes with a recursion-test benchmark that artificially produces such a case. On those larger functions, one step of unwinding could approach 100 nanoseconds even in the cache-hot micro-benchmark. Thankfully such cases are not very common. I perf-recorded a kernel build, so most profiling ticks are in the guts of gcc, and the average number of CFI instructions per frame I see in this profile is around 19. Not great, but not too bad either. SQLite’s benchmark comes in at around 26. So still not a disaster.
In short, we could make unwinding much cheaper, and likely encode much tighter as well. But what we have today is already close enough to be useful in practice and not orders of magnitude worse. We can work with that. Especially given that caching more or less obviates the need for quick DWARF decoding.
Of course, the biggest performance bonus of aw-backtrace is that it unlocks wins of -fomit-frame-pointer.​

Sunday, February 15, 2015

Visualizing perf profiles using pprof

Perf is a pretty powerful profiling tool (in addition to it's other features). But it is not as good as pprof at visualizing profiles.
Now you can capture profiles using perf (including profiling running processes), but deal with them using pprof. With help of perf2pprof tool. It is also available via rubygems. Simply do gem install perf2pprof and you're ready to rock.

Friday, December 12, 2014

Better productivity writing golang code with help of supermegadoc

At work I'm writing more and more golang code recently. It is nice little language. But as a language noob I have yet to internalize which features are available in standard library and how to use them. So I'm spending a lot of time staring at pages under golang.org/pkg looking for types or functions or methods I need.
For my Erlang work I'm quite regular user of supermegadoc erlang integration (video demo with my nice/odd accent is here). And I was seriously lacking something similar for go. Today I finally spent few hours and I have something that looks like what I need.

Observe:

I can quickly find things that are visible to godoc. Just like with other supermegadoc integrations, I can see function signatures and constant and variable values. And I can see if type is struct, "typedef" or interface. From experience working with Erlang's supermegadoc, I know that this means that I often don't even need to open corresponding doc entry. It is often enough to see that it's there and (in case of functions or methods) what it's signature is.

I expect my golang productivity to increase.

Have a nice day folks!

Monday, December 9, 2013

Massive power of (liblzma based) XZ archiver

I've recently restarted gathering of bitcoin market data. I'm grabbing samples of market depth every 3 seconds and I'm collecting trade events.

Market depth samples can be quite large. Every mtgox sample appears to be about 90 kilobytes big. So 1 hour of samples is about 100 megs of data. And month is about 3 gigs. Which is a bit too much.

gzip is able to compress that about 5x. But that's still a bit too large.

I've found xz to really shine on that kind of data. More than 1 gig of data gets squeezed down to less than a meg! And what's extra cool is xz is very quick to decompress. For static data like btc market archive that's very useful.

So quality compression does matter. And I just wanted to express my ultimate respect to authors of that extremely useful software.

Have a nice day and happy hacking!

Sunday, September 22, 2013

Playing with Intel TSX

I've recently got access to a box that has Intel Haswell CPU inside. And I was quite looking forward playing with one of it's most interesting features: hardware transactional memory. My particular interest is to see how cheap it is.

My use case is per-processor data structures (e.g. malloc caches). And without explicit binding of threads to processors, there's only optimistic way of doing it. Which requires some synchronization to defend against pessimistic case of rescheduling of thread to different cpu. That would look like taking cpu id, locking it's corresponding lock which in most cases would be in cache and uncontended and thus reasonably quick, and then doing something with per-cpu data. So in this approach we always pay some performance price even if majority of actual runs will hit fast-path. Lack of really cheap optimistic locking makes that price significant which makes it less attractive.

So lets return to Intel's implementation of transactional memory (aka TSX). Wikipedia article describes that thing pretty well. My understanding is that it's expected to be most useful for somewhat coarse locks where multiple threads would normally contend for the lock yet they touch different memory locations. E.g. imagine different threads touching different buckets of hash table or different branches of binary search tree. It can also be used as compare-and-exchange operation that allows you to process multiple memory locations at once. There's already glibc support for it that optimises pthread mutex operations described in usually nice lwn.net article.

My hope was that this feature ends up being even faster than atomic operations in fastest path (everything is in L1 cache) given it's optimistic nature. And that it might be useful for quick optimistic locking I'd like to have.

You can see my test case here. It simulates fastpath of "lock" that guards a counter. There is no locking itself, just check that "lock" is free. Which is what glibc lock elision code is doing. And you can see how TSX allows to avoid actual locking. "On the other side of the ring" is code that changes counter via traditional compare-exchange atomic operation (no locking either, to give me purer numbers).

On the box I have access to (with Intel(R) Core(TM) i7-4770 CPU @ 3.40GHz processor) I'm getting about 67 cycles per loop iteration for TSX case. And about 27 cycles for atomic CAS code (and same for more traditional locking instructions "xchg mem, register"). Note that it's very likely that larger transactions will have bigger overhead. Also note that usual synchronised region is two atomic ops (unlocking atomic operation being potentially significantly cheaper than locking operation), so in this limited case TSX appears to be somewhat competitive with traditional locking, but not faster.

So TSX is not faster than single atomic operation. Which breaks my hope of using it for quick optimistic lock It is somewhat sad that on today's hardware there's seemingly no way to have fast-path of locks to be lightning fast without playing crazy tricks (e.g. slow path stopping lock owner thread via signal or ptrace like jvm "biased locking" appears to be doing).

Anyways, being slightly more than 2x slower than simple atomic operation is pretty good news IMHO for use cases for which TSX is designed. And it's "multi-word cas" application appears to be very interesting and useful too. So I'm looking forward using it somewhere.

And finally I have to note that, especially at the beginning, debugging transactional memory code can be quite tricky and very weird. That's because transaction is fully isolated while it runs, so there's no way to printf something to see why it fails. Or set breakpoint inside it and inspect things. This hit me initially because my simplistic code wasn't at all prepared to handle transaction failures. I.e. my code is only supposed to test fast-path without any real-world synchronization contention. After few minutes of struggling with it I realized, that even otherwise conflict-less code will abort from time to time. For example, any interrupt (e.g. timer tick) will abort in-flight transaction, as well as in fact any user- to kernel-space transition will.

So lesson number one is that debugging hardware transactional memory code should be done very carefully. Especially if code path is significantly different between successful and abort-ful cases. I.e. imagine some real transaction that might span several layers of code and consider that debugger/printf will never be able to see or expose "guts" of aborted transactions. And lesson number two is that aborts have to be handled always, even in toy code.

Have a nice day and happy hacking.

Sunday, April 8, 2012

gpicker 2.2 is out!

Hello there! I've just made long due release of gpicker 2.2. Some notable changes are:


  • new project type -- script, that I'm using to handle multi-repository project (i.e. couchbase)
  • implemented poor man's isearch on steroid's -- gpicker-isearch
  • big improvements for gpicker-imenu
  • more optimization

Savannah project page has link download area with source .tar.{gz,bz2,xz} archives and binary .deb packages (built on lenny) for i386 and amd64. If you haven't heard of gpicker before also check out supermegadoc which is very convenient gpicker-using tool.

Thursday, December 15, 2011

Me and Gnome3

Hi. Quite a bit of time passed since my last past. That was busy time with continued hard work on (still forthcoming) Couchbase Server 2.0 release and, most importantly, I've found beautiful girl and got married!

Anyway, I just got remind that I should not forget about writing something from time to time. And today's "hot" topic is Gnome 3.

About a month ago (or was it 2 ? Time flies so weirdly with so much happening around me now) Debian Sid got Gnome 3. Even earlier it got some components of Gnome 3. Most noticeable was upgrade of gnome-terminal to Gnome 3 version. And that was almost immediately reverted back to gnome-terminal 2 from last Debian stable. The reason is very simple. Default theme of gtk3 (which is, naturally, used by all gnome 3 apps) is ugly. Like very very ugly. And, surprisingly, there's only one non-default theme engine for gtk3. The one that's heavily using CSS3. I don't like it's look either, but the most worrisome aspect of it is quite noticeable slowness. There are ways to adjust look with CSS3 hackery after all. I've found that some porting work of old gtk engines was initiated. But quick and minimalistic Mist engine I'm used to is not yet ported.

That's basically my whole Gnome 3 story. I cannot tolerate Gnome 3 not because of it's experimental UI, but because I need usable gtk3 theme first. I cannot even say what I'm thinking about gnome's UI, because I haven't even tried using it on daily basis.

Whoever makes Mist work on gtk3 will become my hero. Meanwhile, I was forced to find refuge in XFCE land, that's missing few things I had on my gnome 2 desktop.