News for hackers. Stuff that matters.
Re:Rust is now overall faster than C in benchmarks by frutiger on Sunday January 03, 2021 @04:55PM (#25625612)
Mostly agree with your comment but linear search through arrays of size less than a few hundred will typically beat more sophisticated structures such as red-black trees or hashtables. This is due to prefetching and avoidance of unpredictable pointer traversals. Asymptotic complexity is only that: asymptotic.

In many programs in many domains the sizes of these data structures will rarely exceed this limit.

Re:Rust is now overall faster than C in benchmarks by vbezhenar on Sunday January 03, 2021 @05:10PM (#25625707)
It might be fast, but it'll load CPU caches with that data and it'll evict another useful data. Which means that while this particular code will be fast or at least not very slow, some other code will be slow because its data have to be fetched again.

I have no idea whether that matters or even easy to measure...

Re:Rust is now overall faster than C in benchmarks by josephg on Sunday January 03, 2021 @06:50PM (#25626445)
> I have no idea whether that matters or even easy to measure...

It is reasonably easy to measure, and the GP is about right. I've measured a crossover point of around a few hundred items too. (Though I'm sure it'll vary depending on use case and whatnot.)

I made a rope data structure a few years ago in C. Its a fancy string data structure which supports inserts and deletes of characters at arbitrary offsets. (Designed for text editors). The implementation uses a skip list (which performs similarly to a b-tree). At every node we store an array of characters. To insert or delete, we traverse the structure to find the node at the requested offset, then (usually) memmove a bunch of characters at that node.

Q: How large should that per-node array be? A small number would put more burden on the skip list structure and the allocator, and incur more cache misses. A large number will be linearly slower because of all the time spent in memmove.

Benchmarking shows the ideal number is in the ballpark of 100-200, depending on CPU and some specifics of the benchmark itself. Cache misses are extremely expensive. Storing only a single character at each node (like the SGI C++ rope structure does) makes it run several times slower. (!!)

Code: https://github.com/josephg/librope

This is the constant to change if you want to experiment yourself:

https://github.com/josephg/librope/blob/81e1938e45561b0856d4...

In my opinion, hash tables, btrees and the like in the standard library should probably swap to flat lists internally when the number of items in the collection is small. I'm surprised more libraries don't do that.

Re:Rust is now overall faster than C in benchmarks by loup-vaillant on Sunday January 03, 2021 @08:43PM (#25627121)
> In my opinion, hash tables, btrees and the like in the standard library should probably swap to flat lists internally when the number of items in the collection is small. I'm surprised more libraries don't do that.

If I recall correctly, the STL provides guarantees that prevents it from taking advantage of flat lists. I think some containers (not arrays) guarantee that they don't move the address of whatever they're containing, even if you insert or remove elements. Even if they switched to flat lists, it would be a flat list of pointers, with all the incurred overhead.

Likewise, I believe I have heard of small string optimisation being impossible with std::string for similar reasons.

Re:Rust is now overall faster than C in benchmarks by leetcrew on Monday January 04, 2021 @12:36AM (#25628198)
> Likewise, I believe I have heard of small string optimisation being impossible with std::string for similar reasons.

not impossible, SSO is implemented to some extent in most mainstream c++ compilers.

Re:Rust is now overall faster than C in benchmarks by jstimpfle on Monday January 04, 2021 @09:11PM (#25641209)
I thought that was impossible because it breaks references when moving strings.
Re:Rust is now overall faster than C in benchmarks by leetcrew on Monday January 04, 2021 @10:29PM (#25641721)
I don't see why this would happen. what problems are you envisioning? moves should leave the original object in a valid but unspecified state. that is, a move should not break a reference to the original object itself, but there are no guarantees as to what data it contains afterwards. in the case of a dynamically allocated string, the moved-from object would probably be empty. with SSO, a move is necessarily a copy, so the moved-from string could either be empty or just contain stale data.

are you maybe thinking of iterators? you have to assume iterators are invalid after a move.

Re:Rust is now overall faster than C in benchmarks by jstimpfle on Tuesday January 05, 2021 @03:38AM (#25643472)
No, I think the requirement is that a reference to the actual string buffer must be kept valid when the string is moved. And that property breaks with SSO data, which is not placed in the dynamically allocated buffer - it is placed directly in the object struct (it's an optimization after all), which can't be moved.

EDIT: seems I was wrong, and SSO is allowed for std::string. A similar optimization is illegal for std::vector, though, for the reasons I gave above.

Re:Rust is now overall faster than C in benchmarks by joshuamorton on Sunday January 03, 2021 @08:53PM (#25627179)
I seem to recall some of this related to absl's flat_hash_map and co. They can't be stdlib compatible because there are api concerns that the std implementations provide (but that in practice no one uses) that result in real world performance being left on the table.
Re:Rust is now overall faster than C in benchmarks by slavik81 on Monday January 04, 2021 @01:52AM (#25628477)
Re:Rust is now overall faster than C in benchmarks by frutiger on Sunday January 03, 2021 @10:10PM (#25627577)
Yes, the address of any element of an std::map is valid until that element is erased.
Re:Rust is now overall faster than C in benchmarks by jstimpfle on Monday January 04, 2021 @09:46PM (#25641425)
Thank you, I tried the rope, and I ran a benchmark that creates a rope of length 1G by repeatedly inserting 1023 bytes at random positions. Some notes:

When I changed the hardcoded node size from 136 to 1024 bytes, time went down from 3.6 secs to 2.6 secs on my laptop. It kind of plateaued at 1024 bytes. I didn't do more extensive testing.

What's the rationale for the choice of 136? A cache line is usually 64, so the CPU will always end up loading a multiple of that in any case.

When I did a rope implementation myself (I don't know how it compares in terms of performance) I think I ended up with nodes of 4096 or 8192 bytes size. That was based on a Red-black tree. When I load ~1Gig of data, even with a node size of 4096, there will still be 2^18 nodes, meaning each access requires sth. like 17 child traversals, which seems a lot to me. So I can't see myself going down to 200 bytes or less.

Re:Rust is now overall faster than C in benchmarks by josephg on Tuesday January 05, 2021 @09:28PM (#25654076)
Oh interesting! What CPU was that on? 136 the optimal number I found benchmarking on an old intel based laptop I was using a few years ago. I wonder if modern CPUs (with much larger caches) have changed the ideal array size.
Re:Rust is now overall faster than C in benchmarks by jstimpfle on Wednesday January 06, 2021 @04:55AM (#25656209)
This was on a Sandybridge Laptop from 2011: Intel(R) Core(TM) i5-2520M CPU @ 2.50GHz. If I read right, it has 256KB and 3MB L1 and L2 caches
Re:Rust is now overall faster than C in benchmarks by jimbokun on Sunday January 03, 2021 @10:19PM (#25627618)
Here's an article about how NSArray on MacOS does something similar:

https://ridiculousfish.com/blog/posts/array.html

In the sense of swapping the underlying implementation as the number of items in the collection increases.

Re:Rust is now overall faster than C in benchmarks by scottlamb on Sunday January 03, 2021 @07:18PM (#25626620)
Conversely, more sophisticated algorithms, and particularly monomorphization, may bloat the code size, causing icache, L2/L3 cache, and TLB misses. (Also slows compilation.) I think this is under-appreciated because it doesn't show in microbenchmarks. (I wish I knew of an easy way to measure how much cache pressure you're causing from a microbenchmark.)

I suspect for this reason it'd be better in Rust to use a Go-like hash map implementation [1] that keeps all the key/value information (size, Hash and Eq implementations) in a vtable-like form rather than be monomorphized, except in really hot inner loops where the specialization is worth it. There was an interesting article and discussion on reddit relating to this [2] where someone made a toy type-erased map (though not as nice as Go's) to measure the difference in compilation times. Maybe some day I'll make a more production-ready attempt...

[1] https://dave.cheney.net/2018/05/29/how-the-go-runtime-implem...

[2] https://www.reddit.com/r/rust/comments/g1skz4/an_experiment_...

Re:Rust is now overall faster than C in benchmarks by pbadenski on Monday January 04, 2021 @04:50AM (#25629316)
Thank you for mentioning monomorphization - I wasn't aware of this concept.

Would you maybe if there's any source discussing how this is solved in different languages and performance consequences of it?

Re:Rust is now overall faster than C in benchmarks by bicolao on Monday January 04, 2021 @06:18AM (#25629915)
Not really answering your questions, but regarding monomorphization, you can see a comparison of code size and compilation speed between printf, std::format and C++ iostream. The last one is monomophization, I believe.

https://github.com/fmtlib/fmt#compile-time-and-code-bloat

Re:Rust is now overall faster than C in benchmarks by scottlamb on Monday January 04, 2021 @12:11PM (#25634363)
I'm not aware of a high-quality source discussing this. I'd be interested in seeing one. Off the top of my head:

* In many languages, monomorphization is impractical. Eg, in C it requires macros or external code generators. Java and Go don't even have macros. So it's rarely done. Instead, stuff uses type erasure, dynamic dispatch, more heap allocations, and tables like the ones described in that dave.cheney.net link. Maybe some JITs or even some ahead-of-time compilers do monomorphization internally in some cases, but I'm speculating rather than speaking from knowledge.

* In C++ and Rust, monomorphization is easy. You don't have to do it, but it's easy, so many people do. The containers in the standard library are monomorphized. This code can perform very well in any particular case, but in aggregate it's large enough that I have doubts about whether it's always worth it for the whole program.

Re:Rust is now overall faster than C in benchmarks by forrestthewoods on Sunday January 03, 2021 @05:32PM (#25625855)
You’re stretching there imho.

L1 and L2 are per core. A cacheline is 64 bytes and per-core cache size is on the order of 32kb and 256kb for L1/L2.

Reading data from L1 is on the order of 1 nanosecond (or less) and RAM on the order of 50 nanoseconds.

If you’re scanning an array and load a dozen cachelines that’s almost certainly preferable to several cache-misses (and lines).

Memory access is very often an application’s bottleneck. The answer is almost always more arrays and fewer pointers.

Re:Rust is now overall faster than C in benchmarks by naikrovek on Monday January 04, 2021 @12:40AM (#25628220)
> The answer is almost always more arrays and fewer pointers.

The number of people who dismiss the lowly array is way too high. Arrays are fast. Keep your data in the cache and they're 10x or more faster than normal, and plain flat arrays are almost always faster than literally any OOP nonsense. By "almost always" I mean that I've never once encountered a situation where flat arrays weren't the fastest solution attempted, and I haven't seen everything, so I can't claim they're always fastest.

People really don't understand how fast their computers really are, thanks to developers not caring how fast their code is.

Re:Rust is now overall faster than C in benchmarks by snicker7 on Monday January 04, 2021 @01:04AM (#25628305)
The CPU will load less cache data with linear searches. This is because you will have less cache misses. Less cache misses == less loading from memory. With pointer-heavy data structures, you load more from memory, and most of the stuff you do load is useless.
Re:Rust is now overall faster than C in benchmarks by jstimpfle on Monday January 04, 2021 @09:56PM (#25641497)
A binary tree might require you to visit only O(log2(N)) of your data, while a linear array on average requires you to visit one half. How does that correspond to less loading from memory?

Linear arrays are often faster, not because they require fewer memory loads, but because the cache has intelligence built in (the prefetcher) that loads some memory in advance even before the code requests it. That intelligence works way better with linear array scans than with pointer chasing. (I'm not sure if these loads will count as cache misses or not).

Re:Rust is now overall faster than C in benchmarks by snicker7 on Tuesday January 05, 2021 @10:50AM (#25646747)
You need to look at the coefficients. Memory queries pull in 64 bytes of contiguous memory at a time (actually more with speculative prefetching). That extra data is used in a linear scan, but is mostly wasted when doing random access. You also have other bottlenecks with binary search, e.g. branch misprediction.

Yes, for large enough N, a BST easily beats linear search.

Re:Rust is now overall faster than C in benchmarks by jstimpfle on Tuesday January 05, 2021 @11:24AM (#25647091)
> That extra data is used in a linear scan, but is mostly wasted when doing random access

You are shifting goalposts. What the parent poster said is that a linear scan will read more memory than a binary search, therefore it will cause more churn in the cache (assuming same cache policies, which I don't know is a safe assumption), therefore linear scan may actually be less cache friendly - given that you're not interested in the data in any other way besides for the scan - by way of putting other parts of the program whose data was pushed out of the cache at a disadvantage.

Implied was also that linear scan might actually result in a slower program, even if the scan itself might be faster than a binary search.

What you replied is "The CPU will load less cache data with linear searches", which I assume to be false, although I will be happy to learn that it's actually the case (for example because the CPU has a clever cache eviction policy, executing linear scans by streaming in memory into a small part of the cache instead of thrashing the whole cache).

Let's say we have an array of 200 4-byte ints. Let's do a linear array scan and on average will touch 100 of them (or about 7 cache lines). Now let's say we have each integer in its own linked node in a BST in a totally random memory location. We can expect to need about 7 steps as well (2^8 = 256 > 200), which is 7 cache lines. So about 200 is already a lower bound where BST is more efficient in terms of visited cache lines, even for a pessimistic inefficient data structure like this 4-byte integer example.

Re:Rust is now overall faster than C in benchmarks by ummonk on Sunday January 03, 2021 @07:58PM (#25626861)
This is why Rust uses B-trees, which combine linear search and trees at optimal ratios.