Hacker Newsnew | past | comments | ask | show | jobs | submitlogin

Apart from those benchmark games a lot of real world C is a lot less performant than people think it might be. I spent a fair amount of time reviewing C code in the last 5 years - and things that pop up in nearly every review are costly string operations. Linear counts due to the use of null terminated strings and extra allocations for substrings to attach null terminators, or just deep copies because ownership can’t be determined are far more common than exceptional. This happens because null terminated strings feel like idiomatic C to most people.

Rust avoids those from the start by making slices idiomatic.

Another thing I commonly see is the usage of suboptimal containers (like arrays with linear search) - just because it’s there’s no better alternative at hand (standard library doesn’t offer ones and dependency management is messy). Which also makes it less surprising that code in higher level languages might perform better.



The reason that C programs often perform well is that it’s so incredibly hard to do anything at all in C (especially something reliable) that one can usually only do the simplest thing possible and this typically means simple data structures, simple algorithms and arrays. In many ways, modern CPUs are particularly designed to run the machine code generated by C compilers on typical C code like this. Pointers and memory allocation are often painful or fickle so use of pointer-heavy data structures like linked lists is quite uncommon.

The reason that C programs often don’t perform as well as an equivalent rust program[1] is that it’s so incredibly hard to do anything at all in C (especially something reliable) that one can usually only do the simplest thing possible and this typically means simple data structures, simple algorithms and arrays

[1] suppose the programs are independently created at the same time by programmers of equal ability and are idiomatic in their languages. If a C program is rewritten in rust, it is likely to be faster, but that would likely also be true if it were rewritten in C.


> The reason that C programs often don’t perform as well as an equivalent rust program[1] is that it’s so incredibly hard to do anything at all in C (especially something reliable) that one can usually only do the simplest thing possible and this typically means simple data structures, simple algorithms and arrays

Brian Cantrill talks[1] about exactly this: in his C version he was using AVL trees because they are easy to implement, while his Rust version was using B-trees just because he could. And as a result, his naive first attempt in Rust outperformed his long-optimized C code.

[1]: https://www.youtube.com/watch?v=HgtRAbE1nBM&t=43m15s


So he's comparing the AVL tree he wrote himself to a B-tree optimized to death by someone else?

Out of interest though, does he mention somewhere which actual implementation he used?


Yes, and I suppose that's some of the point: a performant library ecosystem is one hell of a feature.


It sure must be a nice feature - for fast results. Counterpoint, though. I barely used Rust, but when I wanted to play with the Xi editor I got to see the bad places that buying into such an ecosystem can get you to - places formerly pioneered by NPM. I had to download 100s of packages, (IIRC?) there were some build problems, all for something where I don't really see the value of noteworthy dependencies.

If you really need a Btree (like, if you want to make a fair benchmark for a presentation) then you'll absolutely find a reasonable implementation in C. After all, why would you need dependency management for something that should have 0 dependencies?

As an example of library data structures implemented in C - if you want, check out my Red-black tree (not a Btree): https://github.com/jstimpfle/rb3ptr/blob/master/rb3ptr.h . It's really easy to integrate into your project. I think the API (found in rb3ptr.h) is totally usable, and it might also be faster than what you can get with safe Rust - unless you can easily use intrinsically linked data structures in safe Rust.


>The reason that C programs often don’t perform as well as an equivalent rust program[1] is that it’s so incredibly hard to do anything at all in C (especially something reliable) that one can usually only do the simplest thing possible and this typically means simple data structures, simple algorithms and arrays

Reliability is very often the name of the game with C, and part of the reason you might see it written in such a simplistic fashion. In embedded systems, we often follow very strict code convention that has strict requirements on how a C program is to be written. This includes everything from how memory is to be allocated, the maximum number of local variables, the maximum number of the arguments, various limits on a function, constraints to handle errors, etc. We do this because it minimizes potential hazards especially when working with limited memory and system resources.

C is an unforgiving language in that mistakes can occur silently and on mission critical hardware these mistakes can cost more than just your project milestones. These programs are very specialized and often have complex algorithms associated with them. Most of the systems I've worked with include various kinds of feedback control systems, and accompanying algorithms. If you're familiar with control systems, you'll know these algorithms are certainly not trivial by any means.

The challenge with C is more as a developer your C code needs to be perfect. Bugs just aren't an option like they are in other environments. Once a specialized piece of hardware ships it needs to work as intended under a myriad of conditions without powering off for the next 30 years (not always the case but more often than you might think). Sometimes this kind of software is going to be put a position it may have never been designed for. The best we can do is try to add various check/support mechanisms both in software and in hardware, and maintain as safe and hazard free software as possible.

In terms of performance, again coming from an embedded environment, there is almost always a spec we are aiming for. It needs to do X tasks in N time for example. Of course high level design questions like whether to poll or wait for interrupt, or how to broker data from shared resources is decided long before you get to the question of how should I, or if I even should, compare two strings. It is nevertheless advised to go with a trivial solution if the ends satisfy the needs.

Is C time-consuming to write? Is C a "hard" language? These are subjective and depend on the nature of the project. I would certainly never use C with only libc to write a webserver that's going to be serving SaaS Co's backend API.


The irony is that most C developers rebelled against Pascal and similar languages for being programming with a straitjacket, and in the end need to comply with MISRA-C and similar security enforcement regulations to ship anything worth using when human lives are at risk.


And even MISRA C isn't enough to assure the absence of undefined behaviour.

C is a tricky beast to tame.


Not such a big deal in embedded, where the compiler and hardware is always the same, which means that the behavior is still predictable even if undefined


This is not true. Undefined behaviour can mean that

``` bool b; if (b) printf("1 "); if (!b) printf("2 "); ```

might print `1 2 `.


Right on. Unless the compiler documentation promises a consistent handling of the various triggers of undefined behaviour, it is under no obligation to always generate code that handles UB consistently.

To my knowledge there is no C compiler that promises to always handle all forms of UB consistently. The closest you could get would probably be something like Valgrind.


At least for this specific case Microsoft and Google are fixing it by always initializing the variables.


It's not like that is hard to fix, and modern compilers will flag uninitialized variables.

Use your brain and use your tools. C is not really that hard for a large number of problem sets.


Yet, CVE database is full of well known companies that apparently lack such developers.

If their salaries cannot find them, where are they?

Do we have a candidate to prove their secure reports as being wrong?


> It's not like that is hard to fix

In an embedded context, such bugs may be extremely costly to fix, if it's even possible.

> modern compilers will flag uninitialized variables

Reading uninitialized variables is one of the more easily prevented forms of undefined behaviour. As pjmlp points out, undefined behaviour in C/C++ programs is one of the major sources of security vulnerabilities in today's software.


Fascinating. I would’ve thought most commercial C programs would have heavily used linked lists, hashes (dictionary), and binary search trees all over the place.

I assume most C++ programs heavily use more of these advanced data structures, correct?


Heavy use of linked lists is one reason for why commercial C code often is slow. While there are some exceptions linked lists are slower than arrays for most things. Linked lists are easy to implement in C and therefore used a lot.


You can't really compare C with a C++ stdlib or boost, or Qt. Well, some organizations surely have their own repo of debugged, tested, and optimized stuff but I guess a lot of them roll everything on their own again and again - and in C++ there is a rich ecosystem, at least for basic data structures.


The other thing that C++ and Rust have for collections and algorithms is that they follow a standard pattern.

Because of that, it is usually pretty easy to substitute an optimized algorithm or a container for another one with minimal code changes. In C, because there is no standard for how to do containers and algorithms, it is likely that every library does something a little different in terms of conventions making it harder to swap in implementations.


> I would’ve thought most commercial C programs would have heavily used linked lists, hashes (dictionary), and binary search trees all over the place.

Every large scale C code base I have worked on has had these things, and they were used as you say all over the place. If I were to encounter a code base that didn't have these things I would wonder why it was written in C in the first place.


Conversely, I have only seen arrays being used, and never linked lists/hash maps in the few embedded code bases I have worked on.


And I have run across, and implemented, both linked lists, and arrays, in embedded systems. The implementation depended on what made the most sense for the particular problem being addressed.


[flagged]


From the guidelines[1]:

> Be kind. Don't be snarky.

> (…)

> When disagreeing, please reply to the argument instead of calling names.

> (…)

> Please don't post shallow dismissals, especially of other people's work. A good critical comment teaches us something.

You’re being downvoted because your comment doesn’t add to the discussion. You’re (rudely) telling the parent poster they are wrong while providing no basis for the assertion.

[1]: https://news.ycombinator.com/newsguidelines.html


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.


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...


> 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.


> 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.


> 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.


I thought that was impossible because it breaks references when moving strings.


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.


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.


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.



Yes, the address of any element of an std::map is valid until that element is erased.


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.


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.


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


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.


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_...


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?


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


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.


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.


> 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.


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.


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).


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.


> 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.


This is why Rust uses B-trees, which combine linear search and trees at optimal ratios.


> null terminated strings feel like idiomatic C to most people

Doesn't that mean that null terminated strings is idiomatic C? That is, my understanding of the term idiomatic is that it is defined by whatever is most natural to users of a language regardless of whether it is the most performant.


One of my long standing complaints about modern programming is how rarely people read code. We don't encourage it in school, and in the workplace most people only read code written by their coworkers.

It would be the equivalent of teaching people to write books without encouraging them to read anything.

To break myself of the habit I started reading some well regarded programs for fun. And oh boy, have I learned a lot from doing so. One of my first discoveries was this beauty in the Redis source code:

https://github.com/redis/redis/blob/3.0/src/sds.h

The idea is to have a string struct that stores its length and content. But the pointer passed around is a pointer to the (null terminated) contents field in the struct. The string is efficient for internal calls (the length can be queried by subtracting from the pointer). But the pointer is also an idiomatic null-terminated C string pointer, compatible with the standard library and everything else. (typedef char *sds;)

Dovecot is also a gem to read if you're looking for inspiration. The way it manages memory pools is delightful - and I'm sure much more performant than idiomatic rust. (That is, without reaching for arena allocator crates and alternate implementations of Box and Vec).


They're somewhat old now, but I'd recommend checking out The Architecture of Open Source Applications series of books.

https://aosabook.org/en/index.html


My go to troubleshooting method for open source tools I use is to just read the code to figure out what it’s doing (if I can’t quickly figure it out from the documentation). I’ve certainly learnt a lot from doing it, and I’ve even written patches for tools written in all sorts of languages that I’d never used before. The first time I ever wrote Rust and Go code, I was patching bugs in open source projects that I needed to have fixed.


COM on Windows also promoted BSTR as a language-neutral type, which was a pointer to a null-terminated UTF-16 string (compatible with Win32 entry points, at least the Unicode versions) preceded in memory by its length.


This feels like a wrapper around an equivalent of the Linux container_of construct (https://radek.io/2012/11/10/magical-container_of-macro/). I have no idea whether there's further prior art in that respect.


What makes you think arenas are considered unidiomatic in Rust? They’re there to be used when appropriate!


They seem unidiomatic because you have to fight the standard library to use them. Using an arena means abandoning String, Vec, Box, std::collections, and so on.

I have no problem doing that if I need to. But it feels like I'm fighting against the grain of rust more than I'd like.


Per object custom allocators are coming soon! I believe Vec already has an implementation on nightly.


This is one of the things that GATs will solve: it will be possible to have a pointer trait. It's a known language limitation, so you are trying to do the right thing.


This topic is of great interest to me, do you know if there is any related official or community documentation on using arena allocation (and the problems you mentioned) in Rust? I've only found https://doc.rust-lang.org/1.1.0/arena/index.html


Crates like https://crates.io/crates/typed-arena and https://github.com/fitzgen/bumpalo are the way you do this in today’s Rust, but what he’s referring to is that types like String manage their own allocations and aren’t yet parameterizable by an allocator. So they’re not super easy to use together.

In my experience most of the time you need arenas you’re using your own data structure anyway, but YMMV.


> In my experience most of the time you need arenas you’re using your own data structure anyway, but YMMV.

That makes sense for video games. Recently I was goofing with cyrus-imap. I wanted to parse the emails out of an mbox file into JSON (JMAP). Parsing an email with cyrus currently does about 5-10k calls to malloc, but the objects are all extremely short lived - they just have to live long enough to parse and then convert to JSON. This is a perfect case for a bump allocator - I'd love to allocate all the parsed email fields into an arena and then clear the whole thing when we move on to the next message.

Yes, Cyrus uses a ton of its own internal structs for emails, and they're littered with strings and vectors. (Eg for email headers, lists of email recipients, plain text / HTML message content, etc).

Looks like bumpalo will do the job, since it implements its own Box, Vector and String. I understand why, but it seems jarring that I'd need to replace the data types in order to change out the allocator like this. I'm definitely keen for GAT landing if it means bumpalo and friends don't need to reinvent the world to be able to change the allocation strategy.

Edit: Oooh Vec::new_in is in nightly! Exciting! https://doc.rust-lang.org/beta/std/vec/struct.Vec.html#metho...



That is from 1.1.0, which is quite old.


do you have any recommended entry points for reading dovecot's code? I opened it up on github and it's a very large library! thanks for any help you can provide.


Nope not really! It is large, but you don't need to understand the big picture. Explore!

I'd either start at a query entry point, or find the email delivery path or something. Or skim through the files and see if anything catches your eye. Or invent a question for yourself - how does X work (for some X), and see if you can find that part of the code.

Wherever you start, explore around a bit then go deep. See if you can understand for yourself how some small, interesting part works (by tracing out the various structs and function calls). Understanding how the whole program fits together is a separate skill - but don't worry too much about it.

Personally I fell in love with the code in src/lib/memarea and mempool. The way dovecot handles memory pools is super clever. There's also lots of src/lib-X directories, and they're reasonably self contained if you want to skim that list and find something more focussed.

If you haven't read much code before and dovecot feels too big and scary, start with something smaller. I can also recommend reading the sourcecode for git or the code for redis. Oh, and if you do, do yourself a favour and checkout one of the earlier versions of those projects. The lines added early in a project's life are usually more succinct and important compared to lines added later on. So earlier versions are usually better reads. When I read redis, I think I read the code for redis 2.0 or something.


Null terminated strings are often called cstrings. They’re beyond idiomatic; they’re part of the C standard library.


Also Unix. Syscalls return null-terminated strings all over the place. So any language running on Unix has to deal with null-terminated strings.

I know because I run a userland that uses length-prefixed strings as far as possible: https://github.com/akkartik/mu


Not just the standard library, but the core language. They are what you get if you write a string literal in your code.


Yes but if we are being pedantic no. Of course the language does still greatly lean into the null terminated string concept.

You get a null terminated char array but the length is actually available since arrays (as long as they haven't decayed into pointers) can still share their length at compile time.


True. I was skipping over that, but the fact that functions manipulating null terminated strings are part of the standard library is certainly a reason in itself to consider them idiomatic.


They're agreeing with you.


Yes, I understand :)


Also lack of generics can make it slow, e.g. qsort() requires a function call for each comparison. So C++'s std::sort() can be significantly faster on an array of integers.


It's more to do with the fact that std::sort's definition is visible to the compiler and qsort() is not. Put qsort() code in stdlib.h, make it static and write a static intcmp() and you'll see the compiler inline that no problem.


I've done this a few times using http://www.corpit.ru/mjt/qsort.html


Sure you can hard-code intcmp into qsort but then it would only work for arrays of ints.

You could do some macro magic instead of templates e.g. `DEFINE_QSORT(int, intcmp)` which could stamp out `qsort_int` but that's not a part of the stdlib.

C++ arguably gets this right since sort<int> and sort<string> will be separate functions, although templates are of course a footgun. And of course duping the logic for std::sort<T> for a bunch of different T impls increases the binary size.


The poster you are replying to didn't suggest hardcoding intcmp into qsort - just making it so the implmentation of qsort is available to the compiler when the comparison function is known (i.e. just like with C++).

When this is done, the compiler can inline qsort, and replace the indirect function call with an inlined version of intcmp, and then things are equivalent.


Only if inlining qsort is best. Sometimes it is sometimes it isn't, based on complex rules that I trust the compiler to know.


I think mh7 did not mean to hard-code intcmp into qsort. The idea is to move the definition of qsort directly into the stdlib.h header file. That way, the compiler can see the definition of qsort and intcmp at the same time.

In that case, the compiler could make a specialized qsort using intcmp automatically.


I'm skeptical. I doubt that the whole of qsort gets inlined (it's big), and the best the compiler can do is to clone qsort to const-propagate the function pointer for it to get inlined.

In my similar experiments with std::sort with a function pointer only gcc does this with -O3.


I benchmarked it several times in the past and couldn't replicate std::sort being faster (GCC with high optimization settings). Anyway both are slow. If you need fast sort you need an implementation without any function calls (no recursive calls) and both the pivot choice and the chunk size at which insert sort kicks in optimized to your data and hardware. My experience is that you can beat built in sort by 2x to 3x.


How would you perform this optimization? If it’s the same data getting sorted, why not put it in an ordered data structure?


Andrei Alexanrescu has a talk on doing this - he calls them metaparameters e.g. where a hybrid sort chooses to change algorithm.

One library I have exploits the fact that D templates are embarrassingly better than C++'s, so you can actually benchmark a template against it's parameters in a clean manner without overhead - that could be anything from a size_t parameter for a sort or a datastructure for example.

        enum cpuidRange = iota(1, 10).map!(ctfeRepeater).array;
        @TemplateBenchmark!(0, cpuidRange) 
        @FunctionBenchmark!("Measure", iota(1, 10), (_) => [1, 2, 3, 4])(meas) 
        static int sum(string asmLine)(inout int[] input)
        {
            int tmp;
            foreach (i; input)
            {
                tmp += i;
                mixin("asm { ", asmLine, ";}");
            }
            return tmp;
        }
This made-up (pointless) benchmark measures how insert a number of cpuid instructions into the loop of a summing function affects it's runtime. My library writes the code from your specification as above to generate the instantiations and loop to measure the performance. As you might guess, the answer is a lot (CPUID is slow and serializing).

edit: https://github.com/maxhaton/chimpfella - I haven't bothered to add pmc support yet


I love qsort! It's easy to use.

When I once tested std::sort against qsort (sorting 4-byte integer) I measure a 2x difference. So yes, definitely non-trivial, but it won't get much worse than that.

Have you ever seen a program that was slow because of a slow sorting routine?

If you ever need a fast sort (~ never) then the last thing you should do is use std::sort anyway. You should figure out what your data looks like and hand roll an implementation. For example, a radix sort is often possible to use, easy to implement, and much faster than std::sort.


> real world C is a lot less performant than people think it might be

What is meant here? Fast? Reliable? Secure? Memory efficient? Power efficient? Easy to write? Easy to maintain? Quick to compile? Easy to debug?

I have no illusions about the reliability/security/correctness of real-world C code, especially since it's usually not compiled with a memory-safe compiler or run with memory-safe libraries and runtime environments (though it's often sandboxed to limit the damage.) It's relatively easy to introduce memory errors which are not detected by the compiler or runtime.

Certainly many algorithms and data structures (in C and other languages) exhibit tradeoffs including things like speed vs. memory use vs. code size vs. complexity, etc..

But C compilers are pretty fast, which I really like. Then there are/were environments like Turbo Pascal or Think C, which seem to have been amazingly compact while offering a rapid edit-compile-debug cycle as well as decent runtime speed and code size.


I assume, since the submissions says is talking about something "faster in benchmarks", that he means "fast".


Yeah, I think you're right.

"Less performant" seems like a less clear (so to speak) way of saying "slower" (or maybe "less efficient" or simply "worse" in some instances.)


Right. As an industry, we need to get away from the simplistic "manual=fast" model. Logic does not automatically get faster when you write it in C or C++. It frequently gets slower, since your one-off "lean and mean" native code clumsily and slowly does the job that a managed runtime has been tuned for decades to do.


D uses slices for strings, which also gives D a big performance boost over C whenever strings are in play.


Walter, can you elaborate on this, are D's slices functionally equivalent to C++'s string_view? I.e. no copying as long no 'ASCIIZ-dependent' code is involved?


D strings are a simply a pointer/length pair. Substrings can be extracted with no alloc/copying necessary. The length can be determined without loading the string into the cache and scanning it.

Those two features make for big speed improvements.


It is funny when I think of the hundreds of thousands of lines of code I have shipped in my long career (all embedded) and how little of it ever had to handle a string.

I agree, C is a really poor choice for string or other human readable content handling.

But if you have to read-shift-mask-write bytes to hardware control registers, it is a pretty easy language to use, and faster than assembly language in 90% of the cases, with modern compilers. That last note was less true in the mid 1980s, but by the mid-1990s, compiler optimization had gotten to be quite good.

Cellular modem code that runs on DSPs is written in C. Maybe someday that will be written in Rust. We'll see.


Forgive me for asking a stupid question: what does it mean when something is “idiomatic” in a programming language context? Is it just the best or recommended way to do something? Or is it something that’s supported by the language? Or something else?


Idiomatic in a programming language means pretty much what it means in any other language: how would someone fluent in the language express it? In a programming context, that may also correspond to the most performant or otherwise "best" expression, but sometimes it's just a commonly used style or phrase.


A programming language idiom is a pattern or technique that you expect your readers to know, even if it isn't obvious when encountered for the first time. Here's an idiom:

    for (int i=0; i < 10; i++)
We all instantly know this means loop 10 times, from 0 through 9. But on first read it takes a little work to figure out what's happening.


I was struggling with that too initially - replace the word with 'standard' or 'natural' and it retains the same meaning.


Slow code bottlenecked by string operations is probably just bad, though - no matter which way you code your strings. Strings are not what computers like to do. Computers like integers and subscripting operations. Strings can make sense for inter-process scenarios (for example, file paths are usually the right thing and you don't want to reference files with inodes).

There is a culture of bad C code from the 90s - especially C code written in OOP-y ways where that was never necessary. Like, calling malloc() + free() for every little thing instead of more structured memory management. A lot of that code is written in C not with a efficiency or elegance mindset but simply because C was the language that you wrote programs in.


> I spent a fair amount of time reviewing C code in the last 5 years - and things that pop up in nearly every review are costly string operations.

Do not tie computational operations on string operations in C.

Pick one of many string manipulation libraries to do any serious work with them.




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: