On Go, Portability, and System Interfaces
garrett.damore.org
garrett.damore.org
There's one more subtler problem with the "defined to exist on POSIX" thing: strictly speaking, what's defined to exist is a C-language interface. Some interfaces may be defined to use macros, and some platforms may make things work when compiled through the C compiler but not through the dynamic-linker interface. One example that I've run into recently, when binding things in Rust, is the cmsg API, which is primarily defined in terms of macros to walk a heterogeneous array, and has to be reimplemented in Rust (with platform-specific code): https://github.com/carllerche/nix-rust/pull/179. Another example is Android, which before 5.0 (Lollipop) exposed a handful of signal-handling functions as inline functions in a C-language header file, so they were not actually dynamically linkable: https://github.com/rust-lang/rust/commit/a8dbb92b. In both of these cases I would have loved to just #include the C header, but cross-language, that wasn't an option.
It does seem that cgo has some ability to just #include the C header, as demonstrated here. I'd be curious to know if it's powerful enough to handle these two cases.
(Also, the author is totally right in saying that this doesn't belong inline in random application code. If you find yourself writing code like this, it should end up in an abstraction library so that porters have a single place to look, and any application calling `tcsetattr` gains the portability benefits.)
The worst offender I've seen here is Xlib, which has to recreate the internal layout of large structs like Display that have haphazardly grown fields over the decades in order to deal with macros that reach into them: https://github.com/servo/rust-xlib/blob/master/src/xlib.rs#L...
It doesn't look like this has changed, unless this page is out of date: http://xcb.freedesktop.org/opengl/#index5h1
If I'm reading that page correctly, the problem is that GLX is specified as an API + implied ABI, not a wire protocol, and part of that API/ABI contract is "I was compiled with <X11/X11.h> and -lX11, and you'd better give me structures compatible with those"? And half of the GLX implementations are closed-source? Sigh.
What is he ranting on about? The reason Go doesn't link against system libraries is that Go in binary-incompatible with C, as in it's calling convention is different. There is a way to call C through a shim but it's extremely slow which is why they implemented their own syscall interface for Go. Static linking is just a side-effect of this.
Edit: the Go team want to support dynamic linking and it is on the roadmap but not fully implemented as of 1.5 https://docs.google.com/document/d/1nr-TQHw_er6GOQRsF6T43GGh...
Well, by "C calling conventions" it really means "performing a stack switch to get out of M:N, allocating a big stack if necessary or taking a lock to check one out". It's quite a lot of overhead and I wouldn't be surprised if it dwarfs the cost of the SYSENTER/SYSEXIT pair in many cases.
Stack switching doesn't take more than a few machine instructions.
The problem with tcsetattr is that it's a library function, so you (generally[1]) need the target library in order to create a program that calls it. This is bad because in Go we value cross-compiling a lot, so we can't depend on having other people's shared libraries available in the build environment.
Syscalls, however, we know how to do them and we know what they are, so we don't need any special target library in order to generate programs for the target.
[1] On platforms like Solaris and Windows, where we are forced to use target shared libraries, we cheat a little and encode the target symbol names that we need, and we rely on standardized ELF/PE symbol resolution mechanism creating a scheme that's essentially equivalent to having import libraries available. But this is very complicated and takes a lot of code in the linker, so it's not at all unexpected that we avoid it whenever possible.
In Rust we found that stack switching was really slow, even when fully optimized to that scheme. CPUs really don't like it. We thought "it was only a few instructions; how bad can it be"; turns out that any function call overhead can quickly add up to 2x or more overall performance penalties in some programs.
See the performance numbers in this thread for cgo (using the same technique) [1]. According to the first benchmark, calling the function via cgo is 16x slower. You're at 161ns for a no-op function call.
Consider that the cost of a syscall can be below 100ns [2]. The overhead can add up quite a bit!
[1]: https://groups.google.com/forum/#!topic/golang-nuts/RTtMsgZi...
[2]: http://forum.osdev.org/viewtopic.php?p=209429&sid=9bcec5b684...
If the system call doesn't exist, then the program won't compile, because the system call stub won't be defined for that platform.
If the system call is ioctl, and the user tries to call with an request that doesn't exist, the program won't compile because the request will not be defined.
But we don't even export ioctl! First of all, it's not type safe. We only expose ioctl wrappers that are type-safe and use requests defined by the target platform, not arbitrary integers passed by the users.
Of course if the user does an indirect system call, and mucks with unsafe, and passes it arbitrary data, than that's a user problem, not a Go problem.
Using ioctl internally by Go is the right thing.
Users indirectly calling ioctl and breaking type-safety with unsafe is just bad code, but that doesn't have anything to do with Go. Go won't prevent you if you want to shoot yourself like that.
But the code is bad. If you have to call system calls through the indirect system calls and use unsafe to pass parameters, you either do something wrong, or we did something wrong. In this case the author of that code is excusable. It was our fault. We didn't expose the necessary type-safe wrappers, so the authors was forced to write code like that.
This is changing however, just yesterday I committed support for termio-related stuff for Solaris. I assume Linux, BSDs and the other systems will follow very soon.
Even worst case, if calling overhead is a problem, the Go team can likely improve it at some point, but they are less likely to magically map special cases of syscall.SYS_IOCTL to tcsetattr for the sake of portability.
So the alternatives are between doing a trivial system call on the Go stack, or doing a complex stack switch operation that does the (too complicated) System V calling convention ABI translation, and then calls a function that does a system call...
I'm sorry, but the system call "calling convention" is different too. This argument doesn't hold water. Calling convention translation is a trivial issue. Go doesn't usually use system libraries in order to minimise dependencies and to allow easy cross-compiling.
> There is a way to call C through a shim but it's extremely slow
Not really, it's actually pretty fast. Of course it will be always slower than just a regular function call, but it's sufficiently low-overhead that in most cases you can ignore the overhead. Of course there are still cases where you can't ignore the overhead.
On systems like Windows and Solaris, the cgo mechanism is actually used to do back-end "system calls" all the time.
Sufficiently different to not be a problem? As I understand it system calls don't usually rely on the stack whereas C calls do, and it is the stack mismatch between C and Go that is the issue.
> but it's sufficiently low-overhead that in most cases you can ignore the overhead
Maybe for user called function...but if every single system call had to swap out the stack surely that would have a pretty heavy impact...
There is the problem of the stack itself. The C model envisions a large, contiguous stack, possibly with some kernel magic to grow the stack as you approach the end (by handling page faults). Go previously used "segmented stacks", essentially a linked list of heap-allocated buffers used as stack, where functions knew how to allocate and deallocate segments when necessary. C doesn't know how to do that, so a C call previously would have to allocate plenty of memory just in case the function wanted to use a bunch of stack space.
Go no longer uses segmented stacks, so that's essentially not a concern. Go still does reallocate stacks, but does so by copying the entire stack, and the stack is basically never freed. So there might be a one-time performance penalty on the first C call (unless the compiler is smart enough to tell when a Goroutine might possibly call into C, and pre-allocate that space), or the first C call after most of the existing stack was used in Go code, but not after that.
tl;dr: The C calling convention is strictly faster than the Go one (but more complicated), and in Go 1.3+ the existing stack is fine to use, so there shouldn't be performance concerns.
(Unless, of course, you think the C library itself is slow. But that's a very different problem.)
Not true, stacks are still freed if utilisation drops below a certain percentage.
> So there might be a one-time performance penalty on the first C call [...] in Go 1.3+ the existing stack is fine to use
Not true, contiguous stacks start even smaller than segmented stacks, also they can't be used for C code at all. Only type-safe code with stack maps can run on the goroutine stacks, because the precise garbage collector has to scan the stack and has to understand where on the stack there are pointers and where there aren't. C code breaks this requirement.
For calls into C, the thread stack is used, which in Go usually has 32kB, but varies on some platforms, for example on Windows, it's 256kB IIRC. The runtime switches the stack (which only takes a few instructions), does the ABI translation and calls the function.
The stack switching mechanism and ABI translation mechanism have trivial overhead, most of the overhead comes from runtime bookkeeping, e.g. the runtime must ensure there are always enough threads to run user-level Go code.
Is this still true of C code that doesn't call back into Go code, like libc itself? The Go garbage collector will never get triggered, and any Go code should treat anything below %esp after the call completes as uninitialized data.
(Also, on Windows, don't actual system calls reuse the same stack, too?)
It seems like if you wanted to design Go for dynamically linking to libc instead of making raw system calls, it wouldn't be difficult.
I should give a more thorough explanation.
If you call C code, the only way that would be useful is if we pass Go-allocated buffers to the C code. This only works if we pass to the C code untyped uintptr's, which must live on the stack because of garbage collector reasons.
In your hypothetical world, you want to call C code on the g stacks. This can only works if the g stacks are copied into a larger stack. The stack copying mechanism inspects the stack map to see which pointers need updating. This stack map is also used by the garbage collector, but I shouldn't have mentioned it in my original post.
The stack copying mechanism can't update the untyped uintptr's living on the stack, therefore you can't call C code on the g stack.
There are other reasons why calling C code on the g stack would be a bad idea but I won't get into them. Final point is, C code is called on the system stack.
> (Also, on Windows, don't actual system calls reuse the same stack, too?)
Which stack and which system calls?
On Windows, target calls are made on the g0 stack, which is the thread stack. These calls are just calls into regular C code, most of it in kernel32.dll, some of it in ntdll.dll. This C code will eventually do system calls. Those actual system calls don't need any stack.
Switching the stack only takes a few machine instructions. Most of the overhead comes from runtime bookkeeping, in contrast ABI translation and switching the stack take a trivial amount of code and time.
> Maybe for user called function...but if every single system call had to swap out the stack surely that would have a pretty heavy impact...
As mentioned, on some platforms, for example Solaris and Windows, this always happens, and even on other platforms, in some conditions, it happens too. Not to mention that the runtime switches stacks for its own reasons all the time. The overhead is totally dwarfed by the cost of doing system calls in the first place.
I'd really hate to be a distribution maintainer in a world where go components are common. Security bug in the TLS library? Time to download 14GB of updates!
This is what some Go designers think of dynamic linking:
Dynamic linking is coming to Go, like it or not; because it's a feature that is needed in certain circumstances, and that has been acknowledged, even by the Go team.
main.go
package main
/*
struct packed {
unsigned char a;
unsigned long long b;
unsigned char c;
} __attribute__((packed));
*/
import "C"
type Packed C.struct_packed
go tool cgo -godefs main.go package main
type Packed struct {
A uint8
Pad_cgo_0 [8]byte
C uint8
}
We lost packed.b field. Go doesn't support packed structs. More info here: https://groups.google.com/forum/#!topic/golang-nuts/UX5srUMt...And indeed this is how the stdlib does it. If the rest of the go community is not adopting this practice then they aren't following good Idioms in their Go code.
Unfortunately, sanity did not prevail today.
I'm not sure Go can fix this without getting rid of their M:N threading.
(and, how does that work when calling into userspace APIs that assume fixed 1:1 thread/stack correlation)
Go uses M:N scheduling, but you still have N threads with (relatively) large stacks. When calling into C code, Go switches stacks, so it runs on the (relatively) large stack where C code can run. Of course there's some overhead associated with this, but system call overhead is large on its own compared to this anyway.
During the system call the thread (obviously) can't run any other Go code, but that's ok, the runtime makes sure there are always threads available to run user Go code (that's one reason why the runtime stands between user code and system calls).