How Swift could potentially be faster than Objective-C
mikeash.com
mikeash.com
The things that can be "faster" are typically not the bottlenecks in Objective-C programs, as the fine article rightly mentions in passing.
On the other hand, a lot of the really slow parts of recent Objective-C "enhancements" are now mandatory in Swift, for example the Automatic Reference Counting (ARC) support, which is actually a big part of the 100x and more slowdowns.
Another issue is that you have copying semantics by default. The claim is that this can make things faster, but for common objects like arrays and dictionaries it first makes things a lot slower (once they fix the completely borked array semantics).
In all of these cases, you have very slow defaults that you must trust the compiler to make fast again. The claim is that the semantics make it easier for the compiler to optimize. This is true, but so far practical experience appears to be that these two don't balance, that is the compilers don't get enough extra information for optimization to compensate for the loss in performance you get from the semantics being expensive by default.
Except in well-chosen benchmarks.
In fact, even in FP languages that have much tighter semantics and therefore much better opportunities for the "sufficiently smart" compiler to do its magic, you tend to see people dropping down to nasty imperative code (effectively C in all but name) to get decent performance.
Swift's semantics are less tight, so I think it's going to depend even more on the programmer writing fast, imperative code, "C in all but name". Again, there are a few things that help the compiler here, but again I doubt they will generally be able to compensate.
I think the biggest issue with Swift performance is that it is focused on helping the compiler, rather than the programmer. The performance model is, at least so far, highly non-linear and non-intuitive. This will hopefully get better, but the semantics I've touched on make it difficult, they are inherently highly non-linear and inherently difficult to predict.
But when they say "copy semantics" (actually I think it was "value semantics") they mean copy on write semantics. There are tricks that they can do to ensure the copy is pretty rare. If the array passed to a function is not used later in the calling function no copy should be needed. If it is not changed in caller or callee then no copy is needed.
Yes there may be a hit at times but it should be fairly rare.
I'm very glad that they are fixing the borked array semantics, I complained pretty bitterly about that.
As for "very slow defaults that you must trust the compiler to make fast again", that applies to just about every language that wants to be fast at all. C compilers generate awful code when the optimizer is turned off.
Yes, the optimizer can take care of it some times, most of the time, ..., but phew!
While C compilers also generate pretty bad code when optimizations are off, it's nothing near this staggeringly awful. Unless you manage to write a longish loop that it can figure out, which is unlikely outside of benchmarks, my experience is you get around 2x or so for straight-line code that's pretty rare these days. This is 20x to 100x and more.
The 2x roughly matches Probsting's Law[1], which says that advances in compiler optimizer tech. double performance every 18 years. That's not a lot, and does not justify trusting the compiler this much. In short, making the semantics slow and trusting the sufficiently smart compiler [2] to fix it has never worked in the past, and I don't see any evidence why this time should be different.
http://blog.metaobject.com/2014/06/compiler-writers-gone-wil...
The argument as to why it can be faster than plain C is very weak, though. It says that if C programmers don't use the restrict keyword, equivalent Swift code can be faster. This is right, but ignores the fact that careful and competent C programmers will put restrict in all the places where performance matters. It's very hard to be faster than plain C, because it has a long, long history, and as a result, the compilers have been steadily improving for a long time. It is also less powerful allowing you to be closer to the actual assembly code that is to be generated.
But with increasingly better hardware, the speed gains of plain C are diminishing. Yes, you can be very fast, but is it worth the trouble of expressing yourself in such a low-level language?
Also, since you can't type-pun (alias) in Swift, you can't do some things that can be done using plain C, so it is unclear how it can be faster.
Type-punning is never necessary in C; it can always be replaced with one or more calls to memcpy( ), which the compiler can elide (clang, at least, is quite good at this optimization). I don't see any reason why this approach wouldn't work with Swift.
Source: I'm fighting this right now in rustc and plan to fix it by just making the frontend emit fewer memcpys.
Swift is much more strict in terms of typing than either C or C++. Pointers are safe, there's no null, etc. It's the strict typing which translates to the compiler knowing more about your program before it runs, and therefore it can do more transformations without changing intent.
I hope/expect the out-of-box performance will improve over time, but I guess it shouldn't be taken as fact that Swift will always be faster than its predecessor.
-a promise made for pretty much every language ever. Just need a couple of compiler tune ups. Not trying to be critical, that just gave me a chuckle.
On most architectures (including x86-64, ARM, and ARM64), the first few parameters to a function are passed in registers
To add to this, the Microsoft 64 calling convention is that the first four integer parameters (which includes pointers), and the first four floating point values, are passed in registers. The System V AMD64 ABI (used by Linux and others) passes the first 6 integer and the first 6 floating point parameters in registers.
ARMv8 convention is that the first 8 integer/pointer values, and the first 8 floating point values (so up to 16 values) are passed via registers.
This isn't all win, of course -- if your code was using those registers, to hold local variables for instance, it needs to save and restore them, or simply avoid them. In most cases it would be the latter given the abundance of registers.
The whole aliasing bit was just weird and detracts from the piece. Anyone who is doing pointer-based-operations in base C is using restrict as appropriate.
I remember the days when C was just yet another systems programming language deemed too slow for any serious work.
EDIT: Always nice to be downvoted by hipsters that lack coding experience from the days C was UNIX only.
I've heard that remark several times. Which languages were faster than C back then, except assembly? Fortran? Algol? BCPL?
The guarantee provided by owned pointers in Rust is stronger than that of `restrict` in C, so we should be able to propagate the must-not-alias constraints better upon inlining.
bool f(int *restrict a, int *b, bool should_modify) {
if (should_modify) {
*a = b;
}
return a == b;
}
Does this function have defined behavior? It turns out that the C spec says that the aliasing restrictions imposed by `restrict` only apply if the memory object pointed to by the restricted pointer has an intervening modification in the scope of the `restrict`; otherwise, the pointers are allowed to alias. Therefore, this function does not have defined behavior for all inputs. If a and b point to valid memory objects and a != b, then the behavior of f is defined regardless of the value of should_modify. If a == b, then f only has defined behavior if should_modify is false.Yes, this is arguably insane, but it's also the spec.
void f(int *a, const int *b, unsigned size) {
for (unsigned i = 0; i < size; ++i) {
a[i] = b[i] * 2;
}
}
This is the LLVM IR generated by `clang -Ofast -c -o - -emit-llvm -S`:https://gist.github.com/zwarich/21cf3a0b2387302ea48b
Notice the `vector.memcheck` block, which is doing a dynamic aliasing check to decide whether to use the vectorized code. If I add `restrict`, e.g.
void f(int *restrict a, const int *b, unsigned size) {
for (unsigned i = 0; i < size; ++i) {
a[i] = b[i] * 2;
}
}
then that dynamic check goes away:https://gist.github.com/zwarich/48fd374becf5f891cb5a
However, if I put the vectorized loop in a function that gets inlined, e.g.
static void f(int *restrict a, const int *b, unsigned size) {
for (unsigned i = 0; i < size; ++i) {
a[i] = b[i] * 2;
}
}
void g(int *a, int *b, unsigned size) {
f(a, b, size);
}
the dynamic aliasing check reappears:https://gist.github.com/zwarich/3e75f17de835dd9064ee
This is because LLVM's loop vectorizer runs after inlining and the standard optimization passes (which are run in a bottom-up traversal of the SCCs of the callgraph, interleaved with inlining). The `noalias` attribute on function parameters, which is what represents `restrict` in LLVM IR, disappears upon inlining. Many other compilers do something similar, although some like IBM's XLC can preserve `restrict` upon inlining.
I was under the impression that C's qsort is often beaten by languages with support for templates / generics? I'm pretty sure they were talking about qsort.
Since compilers don't control malloc, it's pretty hard for them to do any kind of optimization that relies on the physical locations of distinct objects in memory.
Not impossible, but even a very smart compiler would find it difficult to do things that an above average programmer could easily do up front, by using a better architecture/approach to begin with.
-a promise made for pretty much every language ever. Just need a couple of compiler tune ups. Not trying to be critical, that just gave me a chuckle.
It can be true although the "certain cases" may be quite a small set and there are likely to be many cases where C is faster than language X (where X includes Swift).
The whole aliasing bit was just weird and detracts from the piece. Anyone who is doing pointer-based-operations in base C is using restrict as appropriate.
Is that really true? Sure in highly optimised low level code it can be true. But in general code when can the compiler know that there is no aliasing. Isn't this the reason that Fortran can be faster than C in many cases?
Then there's the teeming masses of app developers who aren't quite as awesome as you. You don't care about them, but Apple does. Apple knows most of them couldn't restrict their way into a box ;) But, they still churned out a billion apps that are used by billions of users. Doing whatever we can to trick them into speeding up their code will literally save untold lifetimes of cumulative lag for the users.
Objective-C provides a message passing mechanism that trades speed for convenience. It turns out that programmers often choose convenience over speed because of time pressure.
So, Objective-C (at least on Apple's operating systems) is as fast as C AND faster than C at a few highly specific things.