Asmttpd – Web server for Linux written in amd64 assembly
github.com
github.com
For the uninitiated, might I recommend my: http://nickdesaulniers.github.io/blog/2014/04/18/lets-write-...
Though, this is written in yasm syntax, which is slightly different.
Also, keep an eye out for a blog post on Interpreters, Compilers, and JITs I'm working on (cleaning it up and getting it peer reviewed this or next week)!
update 1 Actually, would the syscall's be different between Linux and OSX? Let's find out, once this builds! hammers away
update 2 Got it building and linking. bus error when run, debugging with gdb.
update3 Can't generate dwarf2 debug symbols for OSX? $ yasm -g dwarf2
update 4 Careful, this tries to listen on port 80 [0] (0x5000 (LE) == 5*16^1 == 80), I would never run any assembly program off the web with elevated privileges. I recommend 0xB8B0 (LE, port 3000).
update 5
> Actually, would the syscall's be different between Linux and OSX?
Looks like yes: http://unix.stackexchange.com/a/3350 These might be close to shim out (OSX and Linux at least share a calling convention, unlink Windows). I'll upstream what I have.
[0] https://github.com/nemasu/asmttpd/blob/master/main.asm#L24
unlink(Windows) indeed...
I'm somehow disappointed (quite unreasonably, of course) that the code uses plain old zero-terminated C strings instead of something more exotic. One of the fun things about assembly is that you get to reinvent basic language features on the fly -- calling conventions, data layout, strings, everything.
https://github.com/torvalds/linux/blob/fb65d872d7a8dc629837a...
(Hence the need for the strncpy_from_user()-function: https://github.com/torvalds/linux/blob/fb65d872d7a8dc629837a...)
If you null-terminate a length-prefixed string, what if there's a null in the middle of the string?
(1) Allow inconsistency, and go with the length prefix in case of inconsistency. You could allow null bytes in the middle, treating it as a normal length-prefixed string, but then why do you null-terminate the string? (Is it so that you can still pass the string to functions that will choke on embedded nulls? Why would you do that?) This is just asking for kernel bugs.
(2) Allow inconsistency and go with the position of the first null byte in the case of inconsistency. If the length prefix is inconsistent with the position of the first null byte, you could go with the position of the first null byte, but then why even have the length prefix?
(3) Disallow inconsistency. You could disallow embedded nulls, but then the length prefix is just there as a place to cache strlen calls? If you're defining a syscall interface and requiring the length and first null to be consistent, then you need to run strlen anyway in order to sanity check what userspace gave you... why not simplify the external interface to just be either null-terminated or length-prefixed?
The length prefix is the only thing you use, ordinarily. The only time the null comes into play is if you've already had a bug.
Think of it like a stack protector.
... but one that doesn't terminate execution, but instead hides your bugs. In most use cases, I'd prefer to find my bugs in the majority of cases, rather than to hide the bugs except for corner cases.
I think even counting number of UTF-8 code points in a string is faster in UTF-8 than in UTF-16, if you're allowed to use SSE2/AVX2/AVX-512, because all UTF-8 sequences start with a byte that has highest bit 0, all other bytes in the sequence have highest bit 1.
So just SIMD vector compare [1] to find all "positive" bytes (highest bit == 0), which gives you a nice mask. Then move the mask to a general purpose register [2] and popcount [3] it. 16/32/64 (SSE2/AVX2/AVX-512) bytes processed at a time, no branches other than loop control branch.
You can use the same idea to quickly scan UTF-8 string to approximately right position to retrieve a given (random) code point index. Still O(n), but with 10-50x smaller constant factor. If that's not enough, you can simply pre-index every n-th code point (say, every 64/128/256th) in a separate array for larger UTF-8 strings. That gives you constant time random access.
[1]: http://www.felixcloutier.com/x86/PCMPGTB:PCMPGTW:PCMPGTD.htm...
[2]: http://www.felixcloutier.com/x86/PMOVMSKB.html
[3]: http://www.felixcloutier.com/x86/POPCNT.html
Note: UTF-8 code points start either 0xxxxxxx or 11xxxxxx. Regardless of this the basic idea should work, just need to do two compares and to bitwise-or the masks. At the end, AVX-512 of course would use mask-register (k0-k7) and AVX2 would probably need to convert the mask in two parts, once for both 128-bit register halves.
Note 2: Thinking about it a bit more, I think it's enough to check if signed bytes are greater or equal than -64 (0xc0)! This covers bit patterns from 11000000 (-64) to 01111111 (127); all the sequences that can start a sequence. So no two compares and bitwise-or needed after all.
It basically counts continuation bytes (which all start 10xxxxxx) and substracts, rather than trying to count characters.
Additionally, if you know how many bytes are in the string, you can remove the check for the null terminator.
Only tested with long 30 MB strings.
Edit: Now at 26%. But it can still be improved more. Both benchmarks are with hot cache.
new_strlen_utf8 12352856 clock cycles
cp_strlen_utf8 47544818 clock cycles
Edit 2: Well, 4x performance 32 bit, but compiling it 64-bit in VS2015RC manages to optimize cp_strlen_utf8 more, almost doubling performance. 45% then. Will try gcc 5, clang, etc. later. And it can still be optimized further.Edit 3: Ended up at 35% (2.9x) execution time for 64-bit and 18% (5.5x) for 32-bit. My version is as fast in 32 and 64-bit, but cp_strlen_utf8 benefits quite a bit from 64-bit mode. Probably memory bandwidth limited at this point, but I didn't profile yet. In any case, it does utf-8 code point strlen at 16 GB/s at this point. CPU is i5-4430 CPU @ 3.00GHz, two memory channels @1600 MHz.
[1]: http://www.daemonology.net/blog/2008-06-05-faster-utf8-strle...
next, your claim that UTF-8 is not well suited for a general purpose string implementation because it is a variable length encoding and therefore addressing a character becomes a linear time operation is incoherrent: UTF-16 is a variable length encoding just as well. come out and say that you want to be lazy and pretend surrogate pairs don't exist.
> but UTF-16 captures really a very large share of actually used code points.
That most characters[1] in use are a single code unit in UTF-16 is meaningless to code that needs to index[2] into a UTF-16 by code point (or grapheme): the only correct way to accomplish this in a typical UTF-16 string implementation is O(n).
[1]: I love emoji, and they are outside the BMP.
[2]: I think you'll find that most code does not need to index into a string. (Though languages that lack iterators on strings will make writing the code without indexing difficult.)
Actually today it might be hard to understand what indexing is, even if you store your string in UCS-32 encoding. There are graphical symbols that may occupy variable number of UCS-32 items. And they are used out there (e.g. flags).
Encoding of an opaque byte array is a different story. E.g. Python 2's unicode vs string.
The original 1984 Elite computer game is famous for its huge galaxy full of planets. Each of them had individual names and descriptions such as "Lave is most famous for its vast rain forests and the Laveian tree grub."
Yet those strings were never stored as plain strings. The game had to run in 32kB of memory, so almost all strings were stored in a tokenized form and expanded using a pseudo-random number generator:
http://wiki.alioth.net/index.php/Random_number_generator
That article shows how the planet description strings were stored and reconstructed on the fly. The base representation for the aforementioned description of planet Lave was only a handful of bytes: "\x8F is \x97"
So I think Elite is a pretty good example of an application written in assembly that didn't have anything like a generic string type.
push rax ; save our registers
push rdi
push rsi
mov rsi, location ; get the pointer to the right place
mov rdi, destination
beginning:
mov rax, [rsi] ; copy contents to the register so we can compare
test rax, rax ; compare our source to itself, if it's zero, it'll set a flag
jz done ; we're done
movsb ; copy the byte, increment the rsi and rdi registers
jmp beginning
done:
pop rsi ; restore our registers
pop rdi
pop rax
In contrast, with a length parameter, we can do push rcx ; using a different register here
push rdi
push rsi
mov rsi, location ; same as before
mov rcx, length ; moving our length into the counter register
mov rdi, destination
cld ; okay our first change. Clearing the direction flag so the copy
; goes from the first byte to the end
rep movsb ; it'll repeat cx times the movsb command, and then carry on
pop rsi ; restoring our registers
pop rdi
pop rcx
Having the length of strings means you can have much more concise code. It makes loops easier, makes your code cleaner, and in some environments, gives a speed boost.In fact the best, alignment-sensitive solution I've never seen written. It would load 2 large words, shift them to target alignment if needed, store, load, repeat. This would guarantee aligned fetch/store and still do whole-bus operations.
I've waited to see an instruction to do this in any machine ever (why do we have to hand-code this kind of thing, when the processor chip KNOWS the best way to get it done?) I've waited 20 years.
It's hard to do this safely for strcpy/strcmp because you might read past the end of the buffer when trying to test against a null terminator. memcmp/memcpy and length-prefixed blocks let you use a Duff's-Device-like construct to test only the last word byte-by-byte.
I've waited to see an instruction to do this in any machine ever (why do we have to hand-code this kind of thing, when the processor chip KNOWS the best way to get it done?) I've waited 20 years.
Look up "enhanced REP MOVSB"; this link may also be interesting reading: https://software.intel.com/en-us/forums/topic/275765
REP STOS (memset) has also gotten the same boost throughout the generations of x86, and if the trend continues I'd expect REP CMPS and LODS to get the same treatment. These string instructions are tiny (1-2 bytes) and yet very powerful; their greatest advantage is that they don't take up the astoundingly large amount of space in the icache that some extremely micro-optimised routines (i.e. ridiculous amounts of loop unrolling) do.
e.g. a really basic example for a web server would be splitting up a URL into a path and query string: both strings can use the underlying URL without any copying.
Bad for storing data in .text, but still a neat hack that shaved whole tens of bytes off the program size.
Java IIRC also uses a length-oriented format for string constants in .class files. [1] It's been a while since I wrote a .java to MIPS asm compiler in C++ from scratch (don't ask).
This is because real-world strings may contain 0 to N NULs and escaping them is too much of a PITA for serialized formats, so it's easier and common to do things like TYPE LENGTH DATA de/serialization. For modern, efficient binary de/ser, check out binc and msgpack [2,3].
0: http://math.uww.edu/~harrisb/courses/cs171/strings.html
1: https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-4.ht...
http://en.m.wikipedia.org/wiki/Consistent_Overhead_Byte_Stuf...
Basically eliminates a null in a byte stream so it can be used as a terminator.
But it doesn't have default documents, different kinds of error responses, TCP_CORK, sendfile() usage, content-range handling, or even request logging. So asmttpd is way more full-featured than httpdito, and it's still under 6K.
(...httpdito possibly doesn't have any bugs, either, though ☺)
Or maybe in CSS. https://news.ycombinator.com/item?id=9567183
I would love to see that.
What are the obvious reasons I'm missing?
This is discussed in detail in most introductory CS books if you would like to learn more.
ASM derives performance from specialization, ASM asks, how often is this code ACTUALLY going to run on another architecture, OS, etc? And then gains performance by not supporting those things via abstractions, etc.
Throw away your CS textbook and run benchmarks, reality dictates theory, not vice versa.
GPU drivers spend a lot of time trying to optimize beneath their corresponding high-level API. This more-or-less equivalent to compiling GPU machine code on the fly based on GPU configuration - that is, very much like optimizing a high-level language.
If everything goes smoothly, the drivers can do a pretty good job of optimizing everything.
However, if you deviate slightly from the "fast path", the whole thing falls off a performance cliff, and because it's a high level language with a secret black-box optimizer behind it you're actually worse off investigating performance issues than you would be if you'd just written things at a lower level. Not coincidently, graphics APIs are moving to lower levels precisely to remove the complexity from the compiler, increasing transparency and making things more predictable.
Now you might suggest that a "sufficiently advanced compiler" wouldn't do that, but such a thing is a fiction. In practice, the compiler is never sufficiently advanced to optimize in all cases effectively.
---
Consider Javascript, where exactly the same thing happens. Your definition of a "high level language" may not include JS, but it's hard to argue it's not higher than ASM.
Modern day JS engines do a pretty good job of optimizing code JIT. However, you make some innocuous code change and suddenly your function is running in the interpreter instead of being optimized (see https://github.com/GoogleChrome/devtools-docs/issues/53 for examples)
If you were using a lower-level language, your chances of falling off mysterious performance cliffs is significantly reduced. Further, you have the capacity to do low level optimizations that your compiler literally cannot do.
So what if your high level language can now do parallel-maps, if it ignores cache thrashing, or hits load-hit-stores or any one of myriad actual performance holes that real code can fall into?
Or you add a field to the objects you're iterating over and the parallel map implementation hits a weird memory stride and perf drops through the floor. How do you even debug something like this in a high level language where all you see is "map()"?
---
I also think your compiling-ASM-to-JS example is a bit of a strawman, FWIW. The parent was talking about how high-level languages yield higher performance than lower-level ones, not about the portability or transpilability of ASM->JS. (A "suitably advanced transpiler" would handle this problem perfectly anyway)
There are situations where "proper system-level design" just doesn't cut it, and even traditional "rewrite this module in C" doesn't work, because there is no single module to optimize, but rather system is being slowed down by many little overheads all over the place. JITs help with this, but they are not always available. It really pays off to switch to the language with less overall overhead and a focus on performance if you find yourself in such a situation.
> and using a low-level language (be it C, C++ or assembly)
C++ is not a low-level language.
I consider that a good thing. Making implementation harder means you'll be forced to be more thoughtful in design; Asm is so explicit and "low density" that you will naturally want to make every instruction count. You won't be easily tempted to make copies of strings, allocate memory, or do frivolous data movement, because those things all take instructions - instructions that you have to write. Even if you're calling functions, you still have to write the instructions to call them and pass parameters every time. You'll be more careful about not doing work that you don't have to.
Contrast this with high-level languages that make copying data around and allocating huge amounts of memory as easy as '=' and '{}'. They're good for prototyping high-level "does it work" types of things and exploring concepts - the "quickly iterating over different ideas" that you mention - but once you decide what to do, are a lot less controllable with the details because of their high-level nature. And the details, the constants in algorithms, do matter a lot in the real world. Moreover, the difference in constants can be so big that even "proper system-level design" in HLLs can't beat a theoretically less efficient design in Asm, because the constants with the latter are miniscule.
See KolibriOS, MenuetOS, or TempleOS for an idea of what Asm can do.
Low level code is not that difficult to optimize... especially not assembler.
Let's just say it's going to be a lot easier to get 200,000 req/sec from asm than from rails.
*Granted, it's the wrong arch for those, MIPS would help me.
Take a simple problem like the FizzBuzz problem, write it as the simple obvious branching style. Now compile it with GCC or Clang (with -O3) and you end up with lookup table (or at least I did a few months back. Semantically equivalent but not literal "word for word" translation.
It could totally implement a DSL... call it "C" for convenience, that generates the required assembly code :-).
As distinct from C, I take it.
:-)
Requests/sec: 100.00 Transfer/sec: 11.91KB
for a JPEG image (200KB) the results are similar:
Requests/sec: 99.86 Transfer/sec: 19.27MB
[trent@ubuntu/ttypts/4(~s/wrk)%] ./wrk -c 1 -t 1 --latency -d 5 http://localhost:8080/Makefile
Running 5s test @ http://localhost:8080/Makefile
1 threads and 1 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 35.00us 0.00us 35.00us 100.00%
Req/Sec 10.00 0.00 10.00 100.00%
Latency Distribution
50% 35.00us
75% 35.00us
90% 35.00us
99% 35.00us
1 requests in 5.10s, 1.57KB read
Requests/sec: 0.20
Transfer/sec: 314.55B
Note the 1 request. For 1000 clients, it's only doing 1000 requests: [trent@ubuntu/ttypts/4(~s/wrk)%] ./wrk -c 1000 -t 1 --latency -d 5 http://localhost:8080/Makefile
Running 5s test @ http://localhost:8080/Makefile
1 threads and 1000 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 307.93ms 552.88ms 1.63s 87.10%
Req/Sec 414.29 439.07 1.34k 85.71%
Latency Distribution
50% 4.11ms
75% 407.63ms
90% 1.63s
99% 1.63s
1000 requests in 5.01s, 1.53MB read
Socket errors: connect 0, read 41, write 0, timeout 0
Requests/sec: 199.79
Transfer/sec: 312.96KBHaving said that, compilers do pretty badly on C-to-simd optimisation. The best you get is loop vectorization if it's really simple logic. You can usually get some pretty good wins there. The fact you lay out your memory for simd usually is a win all of its own due to cache prefetching even if you don't actually use any simd instructions. Compilers need heuristics to manage cache when you know what you're trying to do, (eg when should it use non-temporal writes, for example?) Fast C code is written while having a really clear mental model of the underlying architecture and the assembly that the C will produce with -O3 (or whatever flag is relevant to your compiler) and then checked with -S or objdump -D, profiled with callgrind/cachegrind, perf, rdtsc etc...
The compiler really can't "Do it for you" You /can/ use a compiler as one of your tools when /you/ do it. As Randy Hyde points out you can always beat the compiler because you can use its generated assembly language in every case you can't beat, so the absolute worst you get is a tie.
So yeah, you can totally smoke clang, Intel, microsoft and gnu C compiler and get paid something for doing it in certain industries too. :-)
Mike Acton being aggressively opinionated on the subject, but the lecture is really good (despite/because of) the bits you'll disagree with and the manner he'll rub you the wrong way. https://www.youtube.com/watch?v=rX0ItVEVjHc