Spending too much time optimizing for loops
octavelarose.github.io
octavelarose.github.io
In languages like Smalltalk Everything Is An Object and instead of function calls messages are sent between objects. It's very elegant. It's up to the compiler to figure out that "1 + 1" doesn't actually require object allocations and message :+ dispatch. Instead all these abstractions can be stripped away so you end up with a single machine code instruction for the addition. In practice this is absolutely hopeless and the compiler will not be able to transform code that is dynamic in theory but static in practice to correct and efficient machine code.
This is for two reasons.
1) Language semantics. If you have multiple threads running then one thread can rewrite the code running on another thread. This means the compiler will be hamstrung in the assumptions it can make about even the most simple for loop. Either you must actually send messages for every integer addition because another thread might be listening to those messages or you disallow multi-threading. In either case your performance will be abysmal.
2) Wide instructions. Modern CPUs can do much work in parallel thanks to SIMD. In a world where every integer is a heap object you have no chance of SIMD optimization.
It's instructive to look at javascript and python. Javascript engines are much faster than python engines because Javascript doesn't do threads. This means the compiler can do tons of static code analysis. No such luck with Python. If you want to go fast with Python you use numpy, which just calls out to C. Here too the optimizations become possible because the language semantics that are holding optimization efforts back are disregarded once the Python code calls into C.
Most of the code we write in practice is pretty static. Dynamic features like virtual function calls, type introspection, and run-time code generation can be added to static languages without much difficulty. On the other hand, it's extremely hard (if not impossible) to get even 10% of the performance your computer is capable of using a highly dynamic language. In practice you don't even get 1%. I know this is an unpopular message but many people have worked hard at making slow languages fast for decades now and it's just not happening.
A Ryzen 5950X can do something like 5ghz * 4 IPC * 16 cores = 300 billion ops per second. Add SIMD on top of that. It's insane.
[1] https://cstheory.stackexchange.com/questions/9765/the-stalin...
T[]? toReturn = null;
var buffer = length <= threshold
? stackalloc T[threshold]
: (toReturn = ArrayPool<T>.Shared.Rent(length));
/* logic */
if (toReturn != null) ArrayPool<T>.Shared.Return(toReturn);
either directly or via a buffer-like type that does it behind the scenes.ArrayPool<T>.Shared is generally well-behaved in terms of GC, and small lengths will not even hit it, being practically free.
The amortized cost of this is substantially lower than allocating such arrays and then throwing them away: stackalloc, particularly for short lengths, is so cheap it might as well be noise, which is cheaper than still fast array alloc for short length, and as the length passes the threshold to avoid excessive stack pressure, it becomes faster to retrieve pre-allocated array from a threadlocal bucket within shared array pool.
Pretty much the same knowledge that applies to C/C++/Rust applies to C# in such scenarios, save for swapping auto-vectorization consideration with the one for simpler usage of Vector128/256/512<T> (which, in turns, applies to the use of intrinsics in both the former and the latter).
I don't think this is the primary difference. While JS's semantics are bad for performance, Python's are insance. Two examples:
- In Python you can change the meaning of operators, so that 1 + 2 could mean something very different from integer addition. This can happen at any point within a program, so you must always guard against the possibility that basic arithmetic---some of the fastest operations on a CPU---has changed meaning. This slows things down considerably. JS is fairly dynamic but AFAIK it's impossible to change this.
- Python allows inspecting the stack (https://docs.python.org/3/library/inspect.html#the-interpret...) so at any point in your program you have to be prepared to reflect the stack as Python objects. Bye-bye performance.
def add_five(a):
return a + 5
dis.dis(add_five)
And this returns something like: LOAD FAST a
LOAD CONST 5
BINARY ADD
RETURN ACCExample:
>>> class foo(object):
... def __add__(x, y):
... print(f'"adding" {x} and {y} by returning {y}+1', x, y, y)
... return y + 1
...
>>> f = foo()
>>> f + 3
"adding" <__main__.foo object at 0x725f470c4e90> and 3 by returning 3+1 <__main__.foo object at 0x725f470c4e90> 3 3
4
>>> add_five(f)
"adding" <__main__.foo object at 0x725f470c4e90> and 5 by returning 5+1 <__main__.foo object at 0x725f470c4e90> 5 5
6
(Not sure why there's extra garbage printed, my Python is a bit rusty.)1: Threads are still under GIL's (global interpreter locks) because the entire object model is thread-unsafe.
2: The Python interpreter works with full objects, JS interpreters all work with some kind of tagged values (NaN,NuN or SMI-bit tagged)
The biggest thing about Python is that they've never wanted to rock the boat on API compatibility since their strength is the ecosystem so CPython is quite tied down. Compare it to PyPy that is far closer to popular JS impls.
You are probably thinking about Python, but Python is kind of a worst case scenario for compiler optimizations. Almost all languages fare better than it.
About #2, some languages do pack your data behind the scene. It's hard to implement, so this is not very common, but the languages more biased into vector and matrix calculations normally do this.
Languages can't pack your data effectively unless you tell the compiler what data types to use. Array<u8> or Array<s32>? Should ints overflow or not? Unless the programmer specifies what should happen the compiler might pick a data type that is 4x larger than necessary.
Despite all the effort put into javascript engines in the past decades the fastest JS code is cross-compiled C code. Quake II runs perfectly in the browser and it's 140k lines of C. But you can't make games of similar complexity in regular Javascript because the browser would completely choke on it.
I don't know whether shared memory is required for high-performance programs but I do know that python THREADS can share memory.
The GIL means that only one thread is executing bytecodes at a time. That's it. It doesn't place any other restriction on python programs.
I write crypto trading code in python. It takes 0.1ms from the time I receive a packet until the time I send the packet containing the trade based on receiving that packet. Yes, that 0.1ms includes the logic to decide whether to trade.
FWIW, that chain of processing happens to involve two threads that share memory.
I am appalled that the top 150 interview questions are 80% iteration avoidance (if you are aiming for good timing).
My experience has always been the problem is how you handle API calls, organize queries, etc... I have maybe 5 times in my career been like 'oh wow we need to avoid this for loop'. Then of course there is managing complexity...
I would be sad, if you couldn't get a Phd for stuff like this.
[1] Sadly enough, the current possibility of fake research means that even publishing to arXiv requires some support from the current researcher.
It's becoming harder and harder because of APC fees :(
>For the majority of IEEE’s fully open access journals, the article processing fee will be $1,995 USD. Some exceptions apply for certain titles, see individual journal author instructions for specific details.
Reviewers don't get paid, journal editorial board members usually don't get paid, and it's all online so publishing is basically free.
Rent seeking at its finest!
1. An obvious exaggeration but somewhat true: https://xkcd.com/664/
2. Apart from tinkering and actually achieving a result, doing a PhD often also means being able to know why it makes sense to explore the field. So, in addition to actually achieving speedup, you have to study the background of all approaches and being aware of advantages and disadvantages of all of them.
* get peer reviewed.
* mention previous literature (that sucks, but I agree is needed to show an understanding of the topic).
* introduce novel concepts.
The barrier is high.
https://github.com/adventuregamestudio/ags/blob/ags4/Engine/...
Or am I the only one?
It may be that the JVM is simply better at optimizing one type of code than the other.
In my experience, the JVM "likes" dumb code: direct, without abstractions on top. Use static final methods for everything, no inheritance, avoid memory allocation (though the JVM is insanely good at optimising small, short memory usage). In a bytecode interpreter you may be tempted to use "proper" type hierarchies with Ops represented by interfaces and things like that. Instead, using an array of objects of the same type which can hold the "variants" (basically, C-style OOP without vtables) of each Op is what makes things go from slow to fast. The code looks very non-idiomatic for Java, of course, but with care can be made pretty readable.
See https://medium.com/@jadsarmo/why-we-chose-java-for-our-high-... / https://news.ycombinator.com/item?id=24895395
> An improvement can be discussed in the morning, and be implemented, tested and released in production in the afternoon.
Edit: I should point out that byte code interpreters still have a place because they tend to represent code in a much smaller form. Again this is something that will change over time as things like project Lilliput shrink object headers, but it’s unlikely to go away entirely.