HN user

fruneau

471 karma

[ my public key: https://keybase.io/florentbruneau; my proof: https://keybase.io/florentbruneau/sigs/sSnUztkO7yw1KmB6PbjZI1-37dvEeYa4WZBUpuwB7sU ]

Posts12
Comments21
View on HN

Extensions are how new features come to an established language... C11 standard mostly standardised stuff that was already supported as extension by most compilers. Most of the time, extensions are created because features are missing from the language (alignement requirement for example), compiler could do a better job at optimising the code with better hints (noreturn, restrict, strict-aliasing, ...), or we could simply make the job of the developper a bit simpler/safer (nested functions, _Generic, blocks, ...).

IMHO, the main benefit lost by using bleeding-edge extensions is portability, which may or may not be an issue.

Three main reasons for this.

First, we originally chose the C language because it is much much simpler. One can master the language without too much pain, and you end up having much more control on what you are actually doing (there's little chance a line of code does not do what you read from it... while in C++ you may have hidden behavior behind even the simpler operation such a + or *).

Secondly, and this is probably a matter of taste, C++11's lambdas are just awfully designed. Their syntax overloads, with a totally different meaning, some tokens such as []. As in many situations, C++ design committee tends to chose the most complicate possible design, without taking readability into account (maybe conciseness is the main goal of their syntax choices?). On the other, the blocks syntax makes is very clear you are dealing with a function-like object with very similar syntax. The choice was made to have a clean and readable syntax.

Third point, C++11 just didn't exist in 2009. There were drafts but support from compilers was just nascent. RHEL in 2009 was at version 6 (very young release) which ships with GCC 4.4 (and GCC 4.7 as an experimental toolchain). The most common RHEL was version 5 with GCC 4.1. RHEL officially supports C++11 since RHEL 7 which ships with GCC 4.8 and was release on June 2014.

Thanks. That said, I'm not sure the amount of effort is that huge: having a working rewriter was a matter of days, which makes it far less expensive than rewriting a codebase of several hundreds of thousands of C to a new language.

There's a moment when you realise you should avoid allocations because you discover that allocations are expensive. Then you also realise at some point that you cannot avoid some allocations (e.g. you need to perform some non-blocking networking) and because of the first point, you get a bit frustrated by this: you're forced to do something that is expensive.

That's exactly when you realise that you can get rid of that frustration if you introduce some complex caching, object pools or more complex structures. But the more complex it gets, the more frustrated you become (again). Finally, you end up writing something simple and efficient: a memory allocator that matches the exact allocation pattern of your use case... and you want to allocator to be as efficient as possible.

I do agree. That ASan part of the article starts with the following sentence: "The tradeoff however is that ASan won’t detect errors such as uses of uninitialized variables or leaks that memcheck can detect, but on the other hand it can detect more errors related to static or stack memory."

Only the runtime part is considered to be less feature-complete that valgrind (because the runtime part of ASan only keeps information about allocated memory, not about memory initialization). It is compile-time instrumentation that make it possible to detect stack and global oob.

There are two main issues with alloca: first you cannot deallocate or reallocate the memory, you just append more data to your frame. As a consequence, it is not suitable for dynamic allocations while the t_stack is.

The second drawback is that alloca allocates on the stack, as a consequence it is limited by the size of the stack (a few megabytes on recent linux distribution, and the actual size of remaining stack depends on the callstack, since each frame consumes some stack and may have put huge buffers/alloca on it already). The t_stack has no hard-limit.

Additionally, by being totally separated from the stack, the t_stack provides a flexible alternative to the stack: you have finer-grained control on allocation/deallocation patterns.

As said in another comment, the drawbacks of alloca are explained in the previous article of the series.

I admit that test stress some corner cases (at least some cases that the allocator designer consider as corner cases). That said, malloc has no choice but supporting that use case.

A use case for such pattern is a message-posting with workers: you queue some messages that are later unqueued and processed by a different thread. This is an increasingly common pattern in modern programs. In that pattern the message is allocated in one thread (let say the main one) and processed then deallocated by another thread.

If your implementation of message allocation is malloc-based, then you will stress the exact same code paths the benchmark is stressing.

PHP is used as a small layer that enables talking to our C code from javascript. We have a custom (Protocol Buffer-like) protocol to manage our RPC, the PHP embeds a native module that implements that protocol and exposes a webservice to which our Javascript code can talk in order to provide some valuable user-experience on top of our C-written technologies.

Nowadays there is so little intelligence in the PHP that we only consider it as a pass-through layer.

The objective of the article wasn't to stress the specific case of the 8-bytes allocation pattern, it was about showing that malloc behavior depends on the context. The size of 8 bytes had been chosen because it was small enough to allow a large number of allocation to be performed so that timing were quite accurate in the results, however the main goal was to show the difference between was are called the "contended" and the "uncontended" case: your program may perform properly with single-threaded workload, but poorly in a multi-threaded environment, not because of explicit locking in your code but because some resource sharing is hidden behind the allocator.

Also, the choice of lot of allocations + lot of deallocations pattern was chosen because this is an issue we ran into quite recently: we allocated a huge tree structure progressively and sometimes we flushed it to disk. The flush was quite efficient, but the deallocation blocked the program during approximatively 30s. As a quick fix, we put the deallocation in background, but this slowed the tree construction down by approximatively 50% because of the contention of the allocator. Even if the size of the chunks in the article were not realistic, the results were near-realistic enough to be considered publishable.

I provided (in a separate comment) the result of the benchmarks with a 32-bytes payload (in that benchmark, all the allocators had the same 12% overhead in term of memory, but we clearly see the same performance pattern as with the 8-bytes payload).

I ran the test with a payload of 32 bytes. Here are the results:

Non-Contented test:

* ptmalloc: alloc 39M/s, free 49M/s

* tcmalloc: alloc 42M/s, free 40M/s

* jemalloc: alloc 21M/s, free 21M/s

* t_stack: alloc 110M/s

Contended test:

* ptmalloc: alloc 6.1M/s, free 6.4M/s

* tcmalloc: alloc 25M/s, free 11M/s

* jemalloc: alloc 18M/s, free 6.5M/s

Note that the results are less accurate that those provided in the article since the number of allocations per batch is smaller (due to the limited amount of RAM available on my desktop).

That's said, the article does not pretend benching realistic patterns.