C/C++ vs. Rust Performance
tempesta-tech.com
tempesta-tech.com
> FreeBSD has been supporting C++ modules for a while.
No, we don't. The link provided is to a user forum where someone asks about it, and the first response is "I don't believe that this is a good idea." I don't know why the author would just make up this false statement, but IMO it really hurts their credibility.
I read the whole thing. It doesn't.
I happen to know TFA's sentence is bunk from first-hand experience with the FreeBSD kernel and C++ proposals, but wanted to show that even the linked forum post doesn't really support the author's claim.
https://lists.freebsd.org/pipermail/freebsd-hackers/2009-Feb... https://lists.freebsd.org/pipermail/freebsd-hackers/2006-Jul...
The last one mentions: "There's actually a fair amount of experience with people doing C++ in FreeBSD kernels. People have been doing things with it for about 8 years now."
There are no C++ kernel modules in FreeBSD itself. There is no support for C++ kernel modules in FreeBSD itself (neither build system nor source code). Developer response to C++ kernel proposals (the most recent on private internal mailing lists I cannot share) is cold.
That's a very strange way to put it IMO, I'm not sure what is meant by "weaker" here. They are stricter and as such can require more work to use effectively, but weaker is a weird way to put it.
Words are hard.
* passing integer as template argument - from what I understand some of rust standard library functions can't handle arrays with more than 64 or 32 elements due to this or something similar. And there is active work for adding const generics to rust.
* variadic templates - there is a RFC for this
* passing template as template argument
* template specialization - C++ is somewhat moving away from this
Variadrics are desired by some folks, but aren't a particularly high priority.
Template template parameters are unlikely to land directly in Rust; GATs will be roughly equivalent. And specialization has a nightly implementation, and is highly desired, but isn't done yet.
That’s the main difference in these languages features.
Saying that Rust generics are „weaker“ than C++ templates is technically incorrect, since they are actuall „stronger“.
Using these terms to mean something else is definitely „weird“.
Like you're describing the type system, they may be talking about actual effectiveness.
Like productivity diff between python and others
Basically, I'm not smart enuf to use rust
However, if you look at that part of the article, they justify that statement by linking to this post [0] which is explicitly about how rust actually type-checks generics more strongly. The linked post has the opposite implication of what the author took it to mean, and that link makes it clear that they were referring explicitly to how they're typed, not to "actual effectiveness".
[0]: https://users.rust-lang.org/t/generic-functions-c-vs-rust/21...
Can you provide your definition of strongly typed?
According to my understanding, C++ templates are evaluated at compile time, and the resulting instance has to qualify all the static type checks of standard C++. The deduced types is always checked, and if there is no substitution fullfill the type check is found, it raises a compile error.
The errors are super ugly, as there are no type constraints for template parameters, but funnily enough the template errors in C++ reminded me of playing around with Haskell without using function declarations. I would find it odd to call Haskell though untyped or weakly typed because of that.
It is beneficial to view C++ as a two-stage programming language, where the first stage runs templates and emits instances and the second stage runs them in the actual target. When C++ templates are said to be "untyped", that's really about the first stage [1]. You can't be very sure that templates act as intended for all valid inputs because it can't statically determine types before the first stage. Of course you can still write almost-working templates.
[1] Specifically the template invocation. C++ templates do have types for compile-time expressions and for that reason it was a wise move to move most of them into constexpr functions.
edit: the static/dynamic distinction pointed out elsethread is a much better description.
Uh oh... I love template specialization... what replaces this?
I cannot agree with this. After I learned rust and tried to mix it with C code I believe that Stroustrup made a mistake by making C++ compatible with C on a syntax level. Strictly defined FFI boundary is a good thing. It liberates. It allow to track precisely what boundary APIs are, it makes boundary APIs the separate documented thing.
> An opposite example to use Rust for an Nginx module is CloudFlare's Quiche, an Nginx extension to support QUIC and HTTP/3 protocols. While it's definitely possible to use Rust for such kind of tasks, the guys, besides the FFI code for C/C++ bindings, still had to write some C code to patch Nginx.
I took a look at the patch, and it is not clear to me, that the patch was needed because rust wouldn't allow to do it the other way. It seems that cloudflare split their code into rust and C for some other reason, because there is a LOT of C code, it doesn't seem to me as a glue code at all. I believe it does more than just glue rust module and nginx. I'm not sure what the reason, but I could guess that the idea was to add possibility to use different implementations of QUIC and HTTP/3 in nginx, not just this particular rust implementation. So they extended nginx for this, and then implemented rust module.
I have been trying to find a good way to deal with hybrid memory/disk structures. One example is external sort, but there are a whole class of such structures used for databases (Hash Join, Hash Aggregation, etc.). The idea is to constrain the memory footprint of a structure and efficiently use the disk.
One challenge is simlly knowing how much memory is being used. Another is supporting multiple heaps/arenas/whatever and allocating in the right one. And a third is safely clearing parts of the structure that have been written to disk for later processing (I guess calling the destructors if necessary?).
Is there any work in this area?
If you make a data structure that is meant to use one contiguous chunk of memory and access memory linearly and/or with locality when possible, I would think it would make explicit disk/hybrid scenarios much less necessary.
Compare that to a Grace HashJoin (https://en.m.wikipedia.org/wiki/Hash_join) which partitions both sides (lookup side and hash table side) into matching partition pairs. The partitioning step and reading the partitions from disk are both sequential access. Then, pick up and process one pair of partitions at a time, which will only require a small hash table in memory.
Note that this is still way more efficient even when on an SSD, because an SSD is still block-oriented, and paging entire blocks in/out for a single lookup is still inefficient.
It even helps when the data fits into memory sometimes, because you can size the partitions small enough to fit in cache, which will make lookups even faster.
You can say "don't use hash tables", but that's not a great amswer for a low-level high-performance language like Rust.
In this case it seems like the downsides you mentioned are the downsides of keeping things on disk, not memory mapping (which would only hit the disk on the first load and would automatically use any available memory). Organization to use disk pages effectively would benefit memory mapping as well.
Memory mapping serves a similar purpose as temp files: a way to tell the OS that it can freely page that out to avoid uncontrolled swapping. But you need to do the organization yourself to keep that access sequential, which is hard to do in rust for the reasons that I mentioned.
If you memory map a hash table and have memory pressure, you'll go into the crazy bad scenario of a page-in for every lookup, which is avoidable with better organization.
I think we are basically in agreement but perhaps my point was not clear.
When I talk about organizing the data structure I'm talking about how the data is layed out in the memory map and how locality is affected.
What I'm saying is not that I think all uses are covered by memory mapping, just that data structure layout combined with memory mapping can probably take care of lots of the majority of scenarios.
I did not know that organizing memory layout was difficult in Rust since it is very straight forward in C++.
(1) knowing how much memory a particular structure (like a hash table) is using
(2) managing that memory as a single chunk; e.g. to free the whole thing at once so that you can pick up another chunk that you had previously serialized
It's not impossible in rust, it's just that you have to build up all the pieces and none of it will integrate nicely with the rest of the rust code.
Box and Vec are starting to gain the ability to be parameterized over an allocator.
> Is there any work in this area?
Kind of, but it's slow going. You can support multiple heaps/arenas/whatever, but it means you have to implement your own. As the ecosystem standardizes around the allocator traits (only the global one is stable yet), there will be easier interop.
I believe they're talking about custom allocator support for Box [0, 1]. The author of the PR says that they intend to add per-container allocator support once support for Box lands, starting with Vec [2].
[0]: https://www.reddit.com/r/rust/comments/jgxgpu/box_will_have_...
[1]: https://github.com/rust-lang/rust/pull/77187
[2]: https://www.reddit.com/r/rust/comments/jgxgpu/box_will_have_...
Never touched goto, alloca and branching usually stayed out of hot code through DOD approaches. Not really sure I agree with the conclusion.
Alternatively they didn't talk at all about aliasing/restrict which is one really neat area of Rust because &mut guarantees one of the key aliasing constraints.
It's a bit condescending. I think the article would have been better without that kind of comment.
goto is nasty, but in my experience writing HPC code, it does have some uses that can't be solved as efficiently (i.e. without extra branching) otherwise in some situations.
'foo: loop {
'bar: loop {
break 'foo
}
}The only other time I've seen it be used fairly regularly is cleanup / return handling, where there are other ways of doing that (RAII structures, if you don't mind creating them), but sometimes using goto for that is just simpler / more obvious than having extra structs/classes to handle that.
https://news.ycombinator.com/item?id=9827276
https://mail.mozilla.org/pipermail/rust-dev/2014-March/00914...
block: {
...
if (err){ break block; }
...
if (err){ break block; }
....
}(/s obviously, but also not /s obviously. The serious point here may be that Java or whatever got in the way of defining functions by requiring them to be "methods" stuck in a "class" rather than, you know, just functions.)
Or maybe not functions, one might consider duplicating code. There is DRY principle, but there is WET principle[1] also. Both of them have their proponents, so at least they are not stupid. How WET shows itself in a highly-optimized programs? Did you try it? I have no link, and may be wrong, but I believe that John Carmack argued in a favor of WET, and I wouldn't say that Carmack doesn't bother with a program speed.
I mean, I'm not convinced, that goto is the game changer, I see other ways around the issue, which in some ways are superior to the solution using goto. So I'm not so sure that your way to use goto is not a misuse.
> if a developer is already perfect with writing good code, but (s)he needs to do something special, like the state machine in the article, the restriction just makes the developer's life harder.
The trouble is there are no developers who a perfect. So it is not intrinsically bad thing to make live of a developer harder by introducing restrictions. If it forces developer to codify more of his assumptions about his program, then it is harder to be imperfect.
The thing is that we used hot/cold labels to minimize jumps over the code and improve instruction cache usage, so calling functions isn't good at all. We wanted one big function with is executed as linearly as possible to improve instruction cache prefetching.
I'm unable to find measurements. I mean, the amount of code duplication is not an optimization parameter, the amount of Mb/s of HTTP parsed is. It is obvious that the size of code leads to a greater cache use and slows down a program, but any engineer's decision should be an educated choice between alternatives with different upsides and downsides. Spagetti code with goto jumping between branches is a bad thing from the point of view of maintainability. From other hand constantly overflowing instruction caches also is a bad thing. Which one is worse? How to compare apples and oranges? I do not know how to quantify maintainability (pity), but throughput of a program could be quantified easily.
> The thing is that we used hot/cold labels to minimize jumps over the code and improve instruction cache usage, so calling functions isn't good at all.
Function in C/C++ is a high-level abstraction, would it lead to a CPU executing 'call' instruction or not -- is a choice of a compiler and a programmer.
If we return to a premise of your article, that rust isn't good for a systems programming, I might notice, that many wouldn't agree with you that what you do is a systems programming worth mentioning. It is nice to know how much C++ code could be tuned for a maximum speed, but I'm not sure that I'd prefer hand crafted state machine to a parser generator, even if the latter would be much slower. If I choose hand-crafted state machine, I'd probably do it by splitting code as much as possible into a 'static inline' functions (most of them would be called just once), with meaningful names and some common arguments to make it as easy to understand and to reason about as possible. Yes, I'd sacrifice some of speed by this way, but I need not the fastest program possible, I need program that is fast enough and have other properties as well. There are more then one optimization parameter: it is systems programming.
Of course, your way have it's rights to exist, and probably it has it's niche too, but as I see it, it is a small narrow niche were available computational resources are scarce while throughput needed is high. In the most cases system software needs to be highly reliable, which needs code enabling us to reason about it, audit it, change it. It needs code which could be maintained by other people, not only those who wrote it.
The measurements are covered in slides 23-24 in http://www.tempesta-tech.com/research/http_str.pdf
> goto jumping between branches is a bad thing from the point of view of maintainability.
In our parser we have DSL for the state machine, so we encode which state the machine should go. The HTTP parser is one of the most updated piece of a web accelerator code and we don't struggle on the FSM - every developer in our team updates the code, even newcomers.
If your C code happens to be compileable with a C++ compiler, then either your program is very small or you are writing really shitty C.
Simplest example is that a `void func()` decl in C and C++ have different semantic meanings.
You can also tell that the C++ standard committee agrees with this to some extent since they tend to add C novelties into the C++ standards whenever possible to make it easier to write compatible code, for instance: https://en.wikipedia.org/wiki/C%2B%2B11#Improved_C_compatibi...
What's happened is the direct opposite of C++ where every fad gets added to the language. With C lots of standard proven features don't make it into the language.
It's a bit like saying that operating the windscreen wipers is a subset of knowing how to drive a car. It's strictly correct, but it's probably not where you should start nor where you should put most of your attention if you're looking to get you license.
The Lua interpreter is written in a subset of C so that it also compiles fine as C++. Or, if you prefer, in a subset of C++ so that it compiles fine as C.
Requiring explicit casts, and forbidding implicit conversions, reduces the odds of being surprised by an unwanted conversion. There's a good argument to be made for adding explicit casts to C code even when the language doesn't require them. GCC's -Wconversion flag can help with this.
There's also no ambiguity. You've already explicitly defined the variable's type at that point.
There is harm in implicit conversions catching the programmer unawares. This is a fairly significant cause of bugs in C code, and it's why C++ uses stricter rules, requiring you to always make these conversions explicit, even when using malloc.
I would also argue that there's a hard to quantify harm in making code more verbose. More visual noise makes it harder to parse what's going on. Sure, one redundant cast isn't going to kill you. But many sources of repetition across an entire code base eventually turn it into a slog where the parts you want are obscured by useless noise.
Yes.
> That isn't a significant burden.
But once you do it your "C" code is littered with a lot of unnecessary casts that increase the verbosity of the code and could hide errors. With pre C99 code it would also hide default int errors if the include for malloc gets lost at some point.
How? As I said, the stricter rules of C++ result in fewer type-conversion errors than C's permissive rules. That's the reason for C++'s stricter rules, and it's the reason Ada is even stricter.
As an example, consider
a = b = c;
In a languages with permissive rules about implicit conversions, you have to inspect the variables' types to determine whether this statement is equivalent to b = c;
a = c;
(Even ignoring OOP, that is.)> With pre C99 code it would also hide default int errors if the include for malloc gets lost at some point.
Wouldn't it just fail to compile if you're missing a required header?
edit steveklabnik linked to [0] so I see what you mean now. I think that's a valid point.
Even then, I never use the latest standards of C++, I stay a couple standards behind so I can rely on whatever compiler I pick being able to handle it. Right now I'm using C++14.
Surely you need to modify C code to compile it with C++ and in the article we made an example of the modification. But the point is that the changes are pretty straightforward and small.
It is easy to write C code that is compatible with C++ if you have compatibility in mind from the beginning. Most of time: 1) cast malloc() – you can define macros for less code; 2) avoid C++ keywords; 3) don't use VLA which is not recommended anyway; 4) add __STDC_LIMIT_MACROS for uint64_t etc. Use -Wc++-compat also helps. It is harder to modify existing C code for the compatibility with C++.
And yet the standard practice for games written in C attempting to use Valve's C++ API for Steamworks integration is to switch to using a C++ compiler.
[1] https://github.com/nospaceships/raw-socket-sniffer/blob/mast...
[2] https://github.com/GirkovArpa/raw-socket-sniffer/blob/master...
It is hard to understand how someone could know so many basic facts about the language and still be so thoroughly confused.
None of the reasons cited for not using C++ in kernel code is valid. None of the things claimed to be impossible are.
Example: RTTI. Really, RTTI is hardly ever used in good code. (I used in once in 10 years.) Despite its low use-value, nothing interferes with using it in a kernel.
Name mangling is absolutely no problem; neither would using `extern "C"`, if you wanted to.
Operator new is wholly compatible with kernel allocators.
The Standard Library does have read-write locks, although it is usually foolish to use such a lock in real code. It is trivial to wrap any synchronization primitive you like with a zero-overhead abstraction. Kernels tend to have their own anyway.
Exceptions in kernel threads would work identically the same as in user-level threads.
Static ctor sections could be run by kernel startup code as easily as they are run in regular programs. (Probably one would not bother running static dtor sections.)
I could go on and on, but enough.
This article joins others packed to the brim with out-and-out falsehoods. When you need to rally so many falsehoods to make your case, you end up making the opposite case--except where your audience is easily fooled.
> Operator new is wholly compatible with kernel allocators.
Please read the referenced thread. The things aren't so simple.
They should demand powerful, but too often fail to.
The Linux kernel is a large system. Things needed to work in Linux are not unusual for customizations needed for any large system, that we do routinely. If you think this is a problem, it can only be from lack of experience of large C++ systems.
When people complain about the complexity of C++, it is generally because they have no experience of large systems that demand all kinds of specializations. C++ can be used in a simple way to write simple programs, and then you get to ignore all the knobs and switches. It can also be used in all varieties of specialized environments, and then you find knob and switch settings to make it work for that environment.
Ex. You don't want your interrupt server code use any FPU/SIMD instructions, as you don't want to save and restore the register file for the FPU/SIMD registers for the kernel code. It's just takes to much time. And there are other architectural execution penalty on some CPUs when using large SIMD instruction codes.
We measured simple operating system functions, like page clear with SIMD, and it's just does not worth it even if we only use SIMD instructions in that function and save/restore only the necessary SIMD registers.
Also heavy inter operation between low level assembly and higher level system code ( C ) are essential in kernel. You have to be able to handle the same structures in assembly and C without any misalignment and misaddressing. ( we use special macros to write down kernel structures, that are used in both assembly and C, all of the assembly files are C preprocessed)
And there are other several issues with OS kernel codes (handling special registers, changing address spaces (virtual), invalidating/flushing caches (specially on ARM), managing any kind of exception (hard and soft, page fault, invalid instructions etc. ), real-time handling, kernel context handling if any, etc.).
There are also other issues with the kernel code and I addressed them in the article, why it doesn't makes sense (while still possible) to use C++ in the kernel code.
This is amusing, because a few days ago, there was an article¹ reporting that the C4 garbage collector solved this problem.
Solutions to pervasive, debilitating GC problems are as perennial as the Spring rain. Every year, we get a new crop, but the problems are still there the year after.
What? Rust allocates on the stack by default.
`void foo(int n) { int values[n]; }`
I don't know that that's a bad thing. AFAIK the implementation details are underspecified even in C and compilers kind of do whatever they want (i.e. I've seen some embedded compilers that monomorphize it in multiples of 2 up to a limit and just hope the user doesn't exhaust the stack).
Edit: I'm pretty sure you can create a hacky version using iterators and a recursive function: `fn vla<T>(iter: impl Iter<Item=T>, init: T, index: usize, count: usize) { ... }`
Edit2: Actually he's talking about `alloca` which is a whole other "here be dragons" feature. See the reddit thread steveklabnik linked for some of the pitfalls
BUT:
- it's not implemented (maybe partially? idk.)
- it has unclear/non-trivial interactions with async-await transformation, ~mainly the generator does need support for multiple unsized values, which isn't supported by rust but can be archived through some hacks in that specific case~ Edit: It turns out it's actually not really possible to use unsized values in generator which life across await/yield boundaries, as far as I can tell.
So it's one of the "it's not hear yet" situations.
> - it has unclear/non-trivial interactions with async-await transformation, mainly the generator does need support for multiple unsized values, which isn't supported by rust but can be archived through some hacks in that specific case.
This especially gives me pause. I've thoroughly enjoyed the async/await experience in Rust despite its rough edges but it seems the community's exploration of async/await is still in its early stages. It sounds that something as complex unsized rvalues will take a long while to get right (I don't know what I'm talking about though, so please correct me!)
The problem is that when you create a generator it doesn't yet know the unsized field it only knows it when creating it in a `poll` call. But it only will call poll after pinning the generator at which point you can't really grow the size of it for a bunch of reasons including that growing might move it in memory (realloc) which you can't due to self references...
So you would need the size of the generator when generating it.
Which could somewhat be possible but not really with current rust features, i.e. it would need a lot a special care.
For me the downsides outweigh the upsides of alloca and I usually find other approaches(free list, arenas, etc).
AFAIK, in practice, many C++ compilers support alloca for compatibility with gcc (or more specifically, its C library)
Also, is alloca part of any C standard? I can’t find evidence for that easily. Also, if so, https://en.wikipedia.org/wiki/Stack-based_memory_allocation#... needs updating. It currently says
“The function was present on Unix systems as early as 32/V (1978), but is not part of Standard C or any POSIX standard.”
I also don’t see how it could be part of the standard, as that, AFAIK, doesn’t even specify the existence of a call stack (a compiler is free to allocate call frames on the heap, for example)
In the same way I could totally define alloca in a C symbol and then just extern it into Rust if I wanted to.
[edit]
Realized I was thinking of VLAs but I think the point still stands.
VLAs are different, I think.
AFAIK VLA is already that, didn't Linux de-VLA the kernel a few years back because they were a common security issue and the code they generate is bad / slow?
This is strange takeaway.... I would say that's almost the only thing that matters. They compared one Rust compiler to 3 C++ compilers and picked the best result? Who does that in practice - who compiles their codebase with 3 different compilers and picks the most efficient one for each object file? Also, the compiler can often improve, where as the language itself is much more difficult to improve - the fact that the language lets you to write a more efficient (and more readable!) structure is crucial.
Also, they seem to misunderstand the point of Rust entirely. The main point of Rust is "safety" (w/o sacrificing performance, yes - but safety was the primary design goal). And for a good reason! Systems programming is more about safe systems than it is about fast systems - fast buggy system programs are useless. The authors decry the loss of goto saying it's "good for juniors but too limiting for professionals" - as if professionals aren't humans too! I'm sorry to say that, but whenever I saw that attitude before - it was with programmers that greatly overestimated their skill level.
> The authors decry the loss of goto saying it's "good for juniors but too limiting for professionals" - as if professionals aren't humans too! I'm sorry to say that, but whenever I saw that attitude before - it was with programmers that greatly overestimated their skill level.
The "I know how to use goto [replace with any feature] everyone else is just stupid" reasoning upsets me so much.
However, there article is about FAST programming languages. Which means, and I stated this explicitly at the beginning of the article, that the main factor for the article is the speed of the generated code.
This is why I compared the single Rust implementation with 3 C/C++ implementations. The question was: whether Rust does something unreachable for C/C++? And the answer is "NOT". Also please keep in mind that all the benchmark programs, using the same algorithms, are still coded in bit different ways. And the differences impact performance significantly. I analyzed two programs, in Rust and C, to show the differences.
Look e.g. at Rich Hickey's interviews - when building actual systems, the "theoretically fastest implementation" doesn't even matter that much in practice[1] - actual practical C++ code (written by experts!) can be slower than even Lisp code. I've witnessed this first hand, too; you've witnessed it too - in a micro-benchmark, no less - where Rust surprised you by being significantly faster than 3 state-of-the-art C++ compilers. But instead of concluding that Rust (the language) maybe did something terribly good, you conclude "meh, it's not that the compiler is better, it just got lucky in this case".
[1] I know there are a few domains where this matters (e.g. embedded systems). I'm not saying that "Lisp is better than C++" - I'm saying that your conclusions about Rust are... surprising, at least to me.
Nim offers alloc, dealloc, allocShared, deallocShared
News at 11.
I have one question: the parser in the article uses computed goto, which, as far as I know, is not standard C. Are there many cases where even the standard goto yields significantly better performance?
It is very unlikely that the computed gotos make the parser faster than using standard features of a more powerful language.
Can you explain more on how? Use case I have in mind is bytecode interpretation where computed goto should be a big help.
https://github.com/ncm/computed-goto
The "more powerful language" part is to get language-level tools to compose parser combinators, ultimately resolving down to tail calls.
Regarding computed and simple goto I'd like to reference our early article discussing the parser in standard goto https://natsys-lab.blogspot.com/2014/11/the-fast-finite-stat... . My recent talk at scale https://www.socallinuxexpo.org/scale/17x/presentations/fast-... (watch videohttps://www.youtube.com/watch?v=LQc4er8ng64&feature=youtu.be... and slides at http://www.tempesta-tech.com/research/http_str.pdf ) discusses the parser with the compiler extensions.
We also found hat Ragel parser generator (using the goto state machine) generated somewhat faster large state machines than Bison (switch-drived state machine). However, as pointed out in the SCALE talk goto works better only on big enough state machines.
There are significant language issues with doing something like this. Yes, in theory, you could do something. But it's going to be very unsafe, and very error prone.
One of these days I'm going to write a crate that easily enables doing all the unsafe, dangerous, stupid things that C programmers want to do, including gotos, with no safety checks or variable lifespan verification of any kind. And I'm going to name it "evil". :^)
I will do it, eventually. ^^
Even in C++ I'd prefer that over a goto-based system (unless generated).
I am learning rust but what criteria any language must meet to call itself mature?
People repeat this like Go isn’t designed for use in resource constrained environments.
AVR support is more of a work in progress than the Cortex-M support, but it does exist.
In practice, turning on such optimizations introduces debilitating bugs, because the optimizations are poorly exercised by other languages, and Rust core developers are fully occupied with other problems.
O(n) is the starting point of optimization. Improvement by factors of two or ten are commonplace, relying on details of how modern machines are implemented, and often lately by farming work out to multiple cores or to GPU cores.
Speed of the code is getting increasingly important as memory and storage sizes grow while clock speed stubbornly does not. Until Dennard scaling died, code got faster just by waiting. Now, making code faster takes work.