Hacker News new | ask | show | jobs
by creativemonkeys 1604 days ago
One of C's design principles is to be fast at the cost of safety, just like an F1 formula car. It will let you make fast mistakes.

You drove a Corolla in college, then got a job and drove a cool BMW for several years and now you think you're hot shit, so you hope in an F1 car and not only does it take forever to learn how to drive it, it has to be driven on a special track and the gearbox is different, what a nuisance!

"If only we could add 4 doors, automatic transmission, snow tires, and a trunk to put our stuff in, people won't keep getting into accidents with this car", you say. Right, but then it becomes a BMW. If you want real speed, you need to first go slow and master the car because otherwise you'll crash and burn.

C is messy because real world hardware is very messy. You can't push bytes through the hardware at its speed limit without getting your hands dirty, and we all come out into the real world wearing "class Dog extends Animal" white gloves.

To use C effectively, you should not be coding in C in your mind. You should be thinking in assembly, but your fingers should be typing C code. It's not safe, but if you want to reach 230MPH and accelerate at 60MPH in 2.6 seconds, you better know exactly what you're doing when you hop behind the wheel of that car. It's not for the weak.

10 comments

> C is messy because real world hardware is very messy

Ada was designed for embedded systems specifically and has guards over many of the pitfalls in C. Still, it provides easy access to in-depth low-level control when you need it (assembly, intrinsics, binding variables to specific memory locations, importing C, creating your own custom allocators). The difference is that you write intent, and then paint additional control on top of that. This makes Ada also suitable for higher level applications.

I think the brittleness of C's string handling functions is not a necessary consequence of anything you said. It's just sloppiness and inertia.
I like that analogy. You're saying that C should only be used in competitions, and not be allowed in the real world, right?
I don't see a problem with using C in the real world, but if you're going to attempt to race on the highway and you don't know how to steer clear of potholes, don't go blaming the car when the wheels fly off. The car requires you to know how to drive at high speeds and a lot of people don't know how to, so instead of being honest with themselves, they look around and conclude that it must be the car's fault, because this many people couldn't possibly be that bad at racing.

It's possible to become a better driver to handle the F1 car, just like it's possible to arrive at the same destination driving a Corolla, just 2 minutes later. If you want the speed though, you have to put in the effort.

You're right, we need to draw a distinction between the Real Programmers and the Quiche Eaters. A mere Java or Python user just isn't good enough, they can't write portable assembly like a Real Programmer can.
Absolutely not. Once a user reads "Head First Java" or customizes Django sites, they get their standard issue keyboard and they're ready to start writing interrupt handlers in C. If the code crashes, it must be the language.
The F1 analogy is easy to use against this line of argumentation. Today's F1 cars are way faster than their predecessors. They are also safer, more automated, and in large part faster because they are easier to drive. The racing is more boring, and cars are uglier, but those are different topics.

The idea that you can't maintain the runtime performance of C while innately supporting automated reasoning about invariants/safety just doesn't hold up. The idea is to move the whole Pareto front outward - that's what advancements in theory and technology do.

They came, they saw, and they went away, and C is still the smallest, fastest and most portable language.

I think the only way to dethrone C is to change the equation of what's expensive to do in hardware - accessing memory, and that's not a problem us software guys are going to solve.

I upvoted this because hey, no lies detected.

The problem is that this particular design principle of C is ready for a comfortable retirement in a beach community.

The machismo is probably why you're getting dragged a bit, but the bottom line is that being intimate with the hardware is orthogonal to pointlessly segfaulting. C does both, Zig is aiming for one of these things and I'll let you guess which.

A segfault lets the user know that the developer made a mistake, and where in the code it happened. Blaming C for segfaults is blaming the tool.

C is a small language with a spec designed to adapt to new hardware while remaining fast. The spec is ambiguous in precisely the places where resolving the ambiguity would mean either limiting its portability or its speed. This increases the learning curve significantly and also requires diligence on behalf of the developer, so it's high effort to write.

It's a perfectionist's language, because, if you can steer clear of the known pitfalls, you get a working piece of software that's maximally portable and fast, and fast is still what we want our tools to be.

There is a place for Zig, and Nim and Rust in this world, but there is no world in which these tools make the same trade-offs as C and end up with a faster and more portable (across hardware) language.

They can sacrifice speed to make it more difficult for the developer to make mistakes. They can sacrifice portability to make assumptions that resolve undefined behavior, which would also decrease the burden on the developer, but they will never get all three - correctness, portability and speed, so in that sense, they will never replace C, they can only hope to starve C of developers.

I work with power tools when I have to. A table saw is dangerous, and I won't refuse to use one on that basis. I wouldn't blame a table saw for cutting someone's thumb off.

One of these days I'll have a big project space though, and I'll put a table saw in. That table saw will be one of the fancy ones which destroys blades instead of digits, when the two come into conflict.

> There is a place for Zig [...] but there is no world in which [this tool makes] the same trade-offs as C and end up with a faster and more portable (across hardware) language.

This isn't the bar it needs to clear. It needs to be as fast and as portable. C can be the fastest possible language, and Zig could be exactly as fast (with, LLVM, say), and still be a language I would prefer because of comptime and some design choices which make it harder for me to lose a digit.

> That table saw will be one of the fancy ones which destroys blades instead of digits, when the two come into conflict.

SawStop. You can expect suddenly a lot of tool manufacturers who would have assured you ten years ago that this technology is either dangerous or compromises the saw's usefulness, will over the next ten years offer substantially the same features as the first patents run out.

> segfault lets the user know that the developer made a mistake, and where in the code it happened. Blaming C for segfaults is blaming the tool.

You can't even assume that most things will segfault though; with UB, you're lucky if it segfaults, since it's more noticeable and easier to debug! But there's not guarantee it will do that when you mess up.

> There is a place for Zig, and Nim and Rust in this world, but there is no world in which these tools make the same trade-offs as C and end up with a faster and more portable (across hardware) language.

I don't think anyone is arguing that a language would be faster or more portable, just that one could be written that's equally fast and portable enough to be useful for most things. I'd be happy to let C remain dominant specialized hardware if it means that the OS for my laptop, desktop, and phone can be written in something safer and as fast.

I’m not aware of any case in which unsafe Rust has any overheard over C. The advantage of Rust, then, is that you can restrict your use of `unsafe` to places where you actually care about things like the overhead of bounds checking.
I think your breakdown of a language is a neat idea, the decomposing of implementations by there ‘scores’ in the three areas of correctness, portability, and speed. I think I’d like to replace speed with a performance score encompassing both speed and memory footprint though. I also agree that achieving high ‘scores’ in all three areas is a relative impossibility.

For me, the best language is going to be the one that has a maximum in the performance area and is provably (at least to some reasonable measure) correct. I think portability between execution environments can be a loss for the types of things I enjoy programming.

There's nothing fast about zero-terminated strings. In fact, many operations on them are much slower than sane alternatives, because they first have to scan the entire string to compute its length. You can't even create a temporary substring without either modifying or copying part of the original string. How lame is that? Zero-terminated strings are almost never the best solution, so why are they the language-supported default?

> You should be thinking in assembly, [...]

Well, then you shouldn't by typing in C, because Undefined Behavior coupled with modern C compilers will make sure that what you get is not what you thought. *cough* signed integer overflow * cough*

> You can't push bytes through the hardware at its speed limit without getting your hands dirty

Rust proves you wrong (maybe some other languages, too, but I don't know them as well)

What you're missing is the difference between known issues and unknown issues. You're looking at a language that's been heavily used for 60 years and accumulated a long list of known issues and things not to do, that powers pretty much everything in computers, and you're comparing that with the new kid on the block with a vocal fanbase.

You could invest your time into learning that finite list, or you could invest your time into learning a new language with a long list of _unknown_ issues yet to be discovered - but out of sight, out of mind, right?

As far as runtime speed goes, assuming equal instructions being generated, if Rust spends even one CPU cycle checking array lengths, its generated code will be slower than C's, by definition. You can justify the trade-off ("it checks array lengths for me because I am human and I forget sometimes") or relax the restrictions ("it's not humanly noticeable"), but you can't claim it runs faster or even just as fast, because it's not.

The only thing Rust proved to me is that there was a whole generation of developers who did't mind writing unreadable Perl code who had kids that are equally unaware of how unreadable Rust code is and it'll take a few decades for them to see that, assuming that Rust stays relevant for another decade.

> You could invest your time into learning that finite list, or you could invest your time into learning a new language with a long list of _unknown_ issues yet to be discovered

That's a bad argument, because it could be used against any change or improvement. By that logic, humans should have never even come down from the trees.

> if Rust spends even one CPU cycle checking array lengths

That's the thing: Almost all checks and guarantees which make Rust safer than C are done at compile time and have no negative effect on the generated code.

That's not a bad argument, it's a statement of fact. I'm using it to point out that using Rust carries risk, whether you realize it or not. Just because you've accepted the risk, doesn't mean it is a universally good decision and C is now bad. Maybe coming down from a tree pays off, maybe you get eaten by a jaguar.

People who don't put in the effort to really learn their tools, need tools with training wheels. It's perfectly fine for a language to put in checks to protect you against yourself and be "fast enough for practical purposes", just don't confuse "almost fast" with "always fast". Rust programs have to pay the price for runtime checks because Rust doesn't trust you to know what you're doing.

The first paragraph of this is weird coming from someone who claims to think assembler and type C. Do you realize, or no, that Rust and Zig use LLVM for release code (Rust uses it for everything)? What are these risks you refer to, looking funny?

> Rust programs have to pay the price for runtime checks because Rust doesn't trust you to know what you're doing.

Buddy, you talk a lot of game about knowing your tools. Don't say obviously ignorant stuff about other peoples tools, it makes me think you're bluffing about C.

To answer your question, I'm well aware of the backends used, but using LLVM doesn't mean that the same IR or assembler gets generated. Enjoy the rest of this weekend.
There is nothing about the design of strtol that makes it particularly fast. If anything, the extra checks and accesses to errno (which on modern systems is generally an implicit function call) that are required to use strtol correctly represent unnecessary overhead, though only a trivial amount of it. But mostly it’s just an awkward API design.
C was not designed to be fast. It was designed to be a bit simpler and a whole lot more portable then assembly code. The speed is a biproduct of how it does not try to do anything other then basically mapping perfectly to the hardware.
This would be more justifiable if C had support for vector instructions, which are crucial for high-performance code on modern CPUs.