In-memory key-value store in C, Go and Python
darkcoding.net
darkcoding.net
Otherwise the test is still interesting but is: "what is the best language to write a memcached clone without being an expert in a given language, using a few hours", that still says something about how different the three languages are, but does not say much about what is the best language to implement the system.
Btw in a more serious test another parameter that you did not considered much is very important, that is, memory usage per-key in the three versions, and in general, memory behavior.
A key-value store is system programming. It should be carefully designed, it's not real-world that you accept the default I/O model of the language you are going to use.
p.s. in the case of C it's hard to argue what is the default I/O model. It supports threads, fork, multiplexing, blocking and non blocking I/O, in the same way basically (in a low level way).
System's programming isn't just writing a KV store used by millions. Sometimes it's writing a dedicated calculation server used in a single company. Carefully optimizing your I/O model might take longer than doing the project itself.
The author was being unfair to C. Use epoll or, better yet, libuv. libuv is a simple-to-use library.
I haven't looked at the rest of the code, but if it's anything like the buffering code this is nothing like what you'd expect to see in a decent C implementation.
It's fairy enough to demonstrate that the Go version makes it easy to write decent performing code, but the C version is atrocious (though to his credit it does at least buffer - I've seen so much C networking code that murders performance by doing small read()'s that I want to cry, including for the longest time the MySQL client library).
Of course part of his criticism is also down to not bothering to look for the plethora of C networking libraries that does this and does it well.
edit: (that said I am not sure if the C version is thread safe either I haven't read the docs for the hash table he is using.)
edit 2: (looks like the C version is not thread safe either).
[1] https://github.com/grahamking/Key-Value-Polyglot/blob/master...
The big win is that Go allows you to write straightforward concurrent code but under the hood uses high-performance system calls like epoll.
EDIT: Here's a thread-safe version: https://github.com/jbarham/Key-Value-Polyglot/blob/master/me.... Still plenty fast.
// Synchronize map access between multiple goroutines. type cache struct { m map[string]string sync.RWMutex }
Then, you can just use:
cache.Lock/Unlock/RLock/RUnlock directly. It's clearer. The beauty of Go's mixins.
https://github.com/rahulkmr/Key-Value-Polyglot/blob/master/m...
As far as raw benchmark goes, it runs faster than the go version on my machine:
# Go version. Changed test.py to 10000 gets and sets.
± $ time python test.py
python test.py 0.48s user 0.60s system 47% cpu 2.289 total
# Python epoll version. Changed test.py to 10000 gets and sets.
± $ time python test.py
python test.py 0.20s user 0.26s system 50% cpu 0.903 total
But go version is easier to read and write, compared to Python which requires the knowledge of epoll.Standard disclaimer: Please note that this comparison is highly unscientific, and take the numbers with a grain of salt.
strtok is not reentrant safe. And why use it, when looking only for " ", use strchr. strlen() is used over and over, instead of keeping lengths somewhere. Also comparison to "set" / "get" could be than char by char, or by using the perfect hash generator somewhat faster code (but even by hand it can be made very fast). 'get ' and 'set ' can directly be checked using one uint32_t rather than byte by byte comparison....
And let's not talk about the needless hidden calls to memory allocation, instead of using slabs, or something more appropriate for the task. (strdup so many places too).
But that's all heresy. I'm a video game programmer, give me such code and I'll beat it up, except send/recv. So what? So fucking what?
[1] https://github.com/wmoss/Key-Value-Polyglot
[2] diesel.io
[3] https://github.com/jamwt/diesel
[4] The first run is against the diesel one
wmoss@wmoss-mba:~/etc/Key-Value-Polyglot$ time python test.py
real 0m0.134s
user 0m0.040s
sys 0m0.020s
wmoss@wmoss-mba:~/etc/Key-Value-Polyglot$ time python test.py
real 0m20.164s
user 0m0.096s
sys 0m0.072s
/shameless plug :-)
(for the record, on my machine the go comparison was 97ms vs. 173ms, so pure python + diesel was 1.78x slower)
Here's what I don't understand:
* test.py is sequential: It first does 500 sets then 500 gets, all in one thread, using a single connection to the server.
* The socket handling function (memg.py:handle_con/memg-diesel.py:handle_con) is called once. There is no parallell execution going on.
* So why is the memg-diesel.py code so much faster? What makes the code for sending and receiving data to/from the socket so much faster?
Could someone please explain to me why an epoll-based solution is so much faster?
I have absolutely no problem with letting my stuff fire requests off on 12+ core machines for hours or days on end. And then repeat it. And again.
Production means 24/7 and when I read your less than an hour benchmark on ANY test - well, that's just a not right.
epoll is optimized for efficiently handling large numbers of sockets, but here there is only one socket. There is no reason epoll should be faster at blocking socket I/O than blocking socket I/O; if it is, I blame the kernel.
(Incidentally, here on OS X where there is no epoll, all the solutions performed pretty terribly - a few seconds for 50000 iterations.)
Reducing the number of send calls, in both the C and Python versions, makes them enormously faster. Go is already batching up the writes, hence the apparent speed advantage.
If you strace the client, you see that the "get" case was replying with two send calls, one for the "VALUE" line, another with the value and "END". All the time is consumed with the client waiting to receive that second message. Depending on the client, and I tried a bunch of ways, it's either in 'poll' (pylibmc), 'futex' (Go), or 'recv' (basic python socket). That second receive is about two orders of magnitude slower than the previous recv.
Why does reading that second line take so much longer?
There's more detail here: https://github.com/grahamking/Key-Value-Polyglot/pull/5#issu...
sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
It's an interaction between delayed ACKS and the Nagle algorithm, mentioned on the Nagle algorithm wikipedia page.
I'm learning a lot this week. Thanks again.
Usually the supposedly Go advantages are presented in a way, as if the same are not present in other languages.
C and Python, OTOH, are available pretty much anywhere. Redis builds on say, Solaris, with no problem because the project is written in C and it is trivial to add the needed calls. A KV store written in Go can't support Solaris because Go itself would need to support Solaris first.
Years of tooling centered around C (e.g., autoconf/automake) is what makes most C programs cross-platform out of the box with little or no OS-specific code if you are sticking to POSIX. Until the same ecosystem develops around any new language, authors realize that choice of language alone can immediately limit their cross-platform capabilities.
[1]: http://developers.sun.com/solaris/articles/event_completion....
The gc Go compiler currently supports FreeBSD, Linux, Mac OS X, and Windows. That's at least 95% of the servers out there (probably more). There is code to support NetBSD, OpenBSD, and Plan 9, but we have held off polishing it for Go 1.
This makes the memg.py server > x100 faster. It outperforms a gevent-based implementation by 10%.
See https://github.com/codeape2/Key-Value-Polyglot/commit/cbc53a...
EDIT: It does not outperform the gevent-based implementation. More performance testing indicates that gevent is around 2x faster. But it outperforms the original version by an order of magnitude.
sendall will look something like:
def sendall(sock, msg):
totalsent = 0
MSGLEN = len(msg)
while totalsent < MSGLEN:
sent = sock.send(msg[totalsent:])
if sent == 0:
raise RuntimeError("socket connection broken")
totalsent = totalsent + sentBut it's a overly small test anyway.
I was actually thinking about writing something very similar as an erlang C node just a couple of days a go. I noted that the overhead for storing a mnesia table of 5 million rows of 3 integers was huge - it would take up 1.6gb in memory! If you know the size of the struct, it should pretty easy to make a fast lookup system (assuming the keys are sequential) too.
I wonder if I could wrap this instead...