Late into a career investing in other areas, what's the advantage of becoming a good C programmer? Especially in a time where Rust and Go are viable options?
Late into a career investing in other areas, what's the advantage of becoming a good C programmer? Especially in a time where Rust and Go are viable options?
Go is probably never going to run on microcontrollers due to its very big overhead.
C perfectly matches on top of the hardware of a processor. Every single design decision about C was made with the computer in mind. Memory, pointers, stack, call stack, returns, arguments, just everything is so excellently designed.
Even Linus Torvalds says so! https://www.youtube.com/watch?v=CYvJPra7Ebk
If I were to make one change to C, it would be to completely rip out the #include system and bring a proper modules system. Apart from that, it's pretty much perfect.
My employer runs go on micro controllers. Definitely suboptimal, but as long as the cost of increasing hardware capacity to accommodate Go is feasible then it's a viable option.
The only problem is that computers have changed a bit in the last 50 years, and C largely hasn't. There are a couple issues:
First, C was designed for single-pass compilers, because the PDP-7 it was designed for was too small to actually run much fancier of a compiler. So C is seriously sub-optimal for optimization in a lot of ways (because the assumption was you weren't going to do compiler optimizations anyway), and there are some user-visible warts like forward declarations that are completely unnecessary today.
Second, the relevant questions with regard to CPU performance have changed a lot. Most notably:
- CPU performance has completely outstripped memory perf, so memory hierarchies and locality are everything
- Parallelism everywhere. Multiple cores, but also deeper instruction pipelines and other such things.
The way those things map to C is completely implicit; they don't show up in the language at all, and getting the machine to do what you want requires knowing things that the code wouldn't suggest at all.
I think if the same people had designed a language for a similar niche today's hardware, a lot of things would be different.
I'm utterly confused at this.
It's trivial to layout memory as you please, where you please, very directly, in C. Set a pointer to and address and write to it. Better yet, I can define a packed struct that maps to a peripheral, point it to its memory address from a data sheet, and have a nice human readable way of controlling it: MyPIECDevice.sample_rate = 2000.
Keeping things physically close in memory has always been a strong requirement, as long as cache, pages, and larger than one-byte-memory buses have existed.
Just make sure you don't forget `volatile` in the right places. A lot of codebases end up just using their own wrappers written in asm for this kind of thing, because the developers have learned (rightly or wrongly) not to trust the compiler.
To be clear, it's not that hard to get the memory layout semantics you want in C. But issues around concurrent access, when it is acceptable for the compiler to omit loads & stores, whether an assignment is guaranteed to be a single load/store or possibly be split up (affects both semantics in the case of mmio and also atomicity), are all subtle questions, the answers to which are not at all suggested by the form of the code; The language is very much designed with the assumptions that (1) memory is just storage, so it's not important to be super precise on how reads and writes actually get done (in fairness, the lack of optimization in the original compilers probably made this more straightforward), and (2) concurrent access isn't really that important (the standard was completely silent on the issue of concurrency until C11). If you care about these issues there's a lot of rules lawyering you have to do to be sure your code isn't going to break if the compiler is cleverer than you are. A modern take on C should be much more explicit about semantically meaningful memory access.
I think you can make a sensible argument that wrt hierarchies C is at least not a heck of a lot worse than the instruction set, so maybe I'm conceding that point -- though the instruction set hides a lot that's going on implicitly too. Some of this though I think is the ISA "coddling" C and C programs; in a legacy-free world it might make more sense to have an ISA let the programmer deal with issues around cache coherence. I could imagine some smartly designed system software using the cache in ways that can't be done right now (example: a copying garbage collector with thread-local nurseries that are (1) small enough to fit in cache (2) never evicted and (3) never synced to main memory, because they're thread-local anyway). Experimental ISA design is well outside my area of competency though, so it's possible I'm talking out of my ass. But the general sentiment that modern ISAs hide a lot from the systems programmer and that other directions might make sense is something that I've heard more knowledgeable people suggest as well.
>A modern take on C should be much more explicit about semantically meaningful memory access.
If you are working on concurrent code close to the hardware you’re going to either have to accept a less efficient language or engage in rule lawyering. Unfortunately, granting the compiler license to perform the most mundane optimizations interferes with concurrent structures. Fortunately, with C there are rules to lawyer with, and they actually are simple. No matter what, rules will always need learned.
It wasn't trivial before fixed width integral types, which is fairly recent in C terms (C99), and it's still far more complicated than it needs to be.
Furthermore, the fact that C is the defacto language of performance means that our hardware has been constrained by needing to run C programs well in order to compete.
Think of all the interesting innovation we could have had without such constraints. For instance, see how powerful and versatile GPUs have become because they didn't carry that legacy.
GPUs are the best example for why C is a good lower-level high-level language, seeing how CUDA is programmed in C/C++.
Do you have any examples of architectures that could exist if only they weren't constrained by legacy C?
CUDA is not C or C++. That you can program GPUs in a C/C++-like language does not entail that C/C++ is a natural form of expression for that architecture.
> Do you have any examples of architectures that could exist if only they weren't constrained by legacy C?
Turing tarpit means that every architecture could be realized, but that doesn't make it a particularly efficient or a natural fit for the hardware.
For instance, consider that every garbage collected language must distinguish pointers from integer types, but no such distinction exists in current hardware, and the bookkeeping required can incur significant performance and memory constraints (edit: C also makes this distinction but it doesn't enforce it).
Lisp machines and tagged hardware architectures do make such a distinction though, and so more naturally fit. With such distinctions, you could even have a hardware GC.
It's not a matter of what is/isn't a "natural form of expression." The point of C/C++ is to be high-level enough for humans to build their own abstractions over hardware. (sounds like an OS, right?) The success of the design of C/C++ is in that the creators had no knowledge of modern GPUs, yet GPUs can efficiently execute them with a little care from developers. We use other abstractions (e.g. SciPy on Tensorflow) because they are more appropriate to solve our problems, but they are built on C.
>Lisp machines and tagged hardware architectures do make such a distinction though, and so more naturally fit. With such distinctions, you could even have a hardware GC.
And why would that not be backwards-compatible with legacy C?
Particularly, I am rejecting the idea that C is somehow stunting hardware development - I see no evidence of this fact. I am also skeptical about the claim (although I will not reject it outright) that there is a language substantially better fit compared to C for low-level programming (e.g. embedded, kernel).
Sure it matters. If primitives don't map naturally to the hardware, then you have to build a runtime to emulate those primitives, just like GC'd languages do.
> The success of the design of C/C++ is in that the creators had no knowledge of modern GPUs, yet GPUs can efficiently execute them with a little care from developers
You cannot run any arbitrary C program on a GPU. This fact is exactly why GPUs were able to innovate without legacy compatibility holding them back.
Only later were GPUs generalised to support more sophisticated programs, which then permitted a subset of C to execute efficiently.
The progress of GPUs proves exactly the opposite point that you are claiming. If C were so perfectly suited to any sort of hardware, then GPUs would have been able to run C programs right from the beginning, which is not true.
> And why would that not be backwards-compatible with legacy C?
That's not the point I'm making. Turing equivalence ensures that compatibility can be assured no matter what.
The actual point is that CPU innovations were tested against C benchmark suites to check whether innovations effectively improved performance, and some or many of those that failed to show meaningful improvements were discarded, despite the fact that they would have had other benefits (obviously not all of them, but enough). It's simply natural selection for CPU innovation.
It's incredibly naive to think that only hardware influences software and not vice versa. For instance, who would create a hardware architecture that didn't have pointers? It would simply never happen, because efficient C compatibility is too important.
The problem is that C was given a disproportionately heavy weighting in these decisions. For instance, a tagged memory architecture would show zero improvement on C benchmarks, but it would have been huge for the languages that now dominate the software industry.
> that there is a language substantially better fit compared to C for low-level programming (e.g. embedded, kernel).
The limitations of C are well known (poor bit fields and bit manipulation, poor support for alignment and padding, no modules, poor standard library, etc, etc.).
Zig addresses some of those issues. Ada has been better than C for a long time. A better language than all of these could definitely be designed given enough resources, eg. see the research effort "House" [1].
That's only half the equation. Hardware cannot save you from semantics that are less efficient. To use your example: every GC'd language must have a runtime system track objects, whether that is implemented with or without hardware support. That system constitutes additional overhead -- either precious silicon is used delivering hardware support for GC or clock cycles are used emulating that support. Either way, you're losing performance. C/C++ have semantics that are easy to support, in contrast.
>You cannot run any arbitrary C program on a GPU.
Nor can you run any arbitrary C/C++ program written for Posix on Windows, or a program written for the x86 on a STM32, etc. You have always had to know your platform with C/C++. The point is that they are flexible enough to work very well on many platforms.
>This fact is exactly why GPUs were able to innovate without legacy compatibility holding them back.
GPUs have become a lucrative business precisely because they have begun exposing a C++ interface. Look at how the usage of graphics cards have changed in recenter years.
> If C were so perfectly suited to any sort of hardware, then GPUs would have been able to run C programs right from the beginning, which is not true.
No. GPUs _were not_ general purpose compute devices from the beginning, as you pointed out. You had GLSL, etc. but the interface exposed to programmers was not Turing-complete. From what I gather, GPUs have only had a Turing-complete interface since shader model 3.0, which first appeared in 2004. By 2007, you had nvcc. Today, C++ is very well supported by CUDA. You may as well be saying "You can't run C on a cardboard box, so it's obviously not well-suited to all hardware." Obviously, your hardware needs to expose a Turing-complete interface for a Turing-complete language to be able to run on it.
>The problem is that C was given a disproportionately heavy weighting in these decisions. For instance, a tagged memory architecture would show zero improvement on C benchmarks, but it would have been huge for the languages that now dominate the software industry.
At what cost? As I already pointed out, adding support for VHLLs at the hardware level means you are spending silicon space on that task => languages like C will be slower. Yes, a lot of software is written in JavaScript, Java, and Python, and these languages would benefit from that hardware support. But people using JavaScript, Java, and Python generally are relying on C services (memcached, redis, postgre, etc) to do their heavy-lifting anyway, which you just made slower.
>For instance, who would create a hardware architecture that didn't have pointers? It would simply never happen, because efficient C compatibility is too important.
No. It would never happen because the machine you just described would make a very bad general purpose computer.
>The limitations of C are well known
Yes, they are. But everything you listed isn't substantial. It's C, with a better standard library, standard support for controlling alignment/padding, and modules. That's not significantly different.
Cuda is a C++ API. On modern hardware, it's programmed in purely standard C++.
Being tedious and almost as unexpressive as asssembly does not make a low level language.
C doesn't map to the machine and never did. Compilers and chip vendors map to C.
Further reading: https://queue.acm.org/detail.cfm?id=3212479
The problem with this line of argument: assembly has also not changed. If the things you talk about mattered, we would be using a different assembly.
We are. It's called microcode.
no_std allows the important hooks of panicking, output and allocation to be implemented by the user. It's also very easy to put in hard-coded pointers that represent memory-mapped hardware. And there's no GC. Furthermore, it's entirely possible to convert only a portion of a project to using Rust while working to gradually to replace/reimplement.
> If I were to make one change to C, it would be to completely rip out the #include system [preprocessor] and bring a proper modules system.
Congratulations, you've just reinvented Java, D, Rust and Go.
> Apart from that, it's pretty much perfect.
This seems like a religious opinion rather than having understanding of different paradigms. Have you been paying attention to why Java, Erlang, Go, Rust and exist? Rust has numerous advantages over C that eliminate entire categories of problems without sacrificing speed. If you can't see that, then maybe you don't want to see it.
Another thing is that large C code bases tend to become ensconced in layers of preprocessor macros, which I think is a hack-y way of doing things.
A computer. The PDP-11. C was a terrible fit for a lot of the popular contemporary architectures when it was designed (PDP-10, Burroughs large systems, UNIVAC 1100, CDC mainframes, HP 3000), and continued to be a very poor fit for many computers in the 1980s (segmented 8086 and 286, 6502, AS/400, Lisp Machines except for Xerox, Connection Machine, Tandem, Novix/RTX, Transputer, etc.).
But anyways, programming skills is not so much about the language. A good programmer in one language will be a good programmer in any other language very quickly. But still, if I have to hire a programmer for a project in a language he doesn't know, I will tend to prefer C programmers over those who only use higher level languages. The reason is that C programmers may do things that are not pretty, but they usually understand what they are doing, people who are only accustomed to some higher level language may come up with better designs, but write code that make no sense.
My point is that learning C is definitely worth it. Not only it has real life applications but it will help you become a better programmer in general. But it doesn't mean you should limit yourself to C. Go and Rust are good too, and even the most hated languages (ex: PHP) can teach valuable lessons.
This is not true. It hasn't been true for decades. C is an abstract high level language on modern processors.
As a C++ person, basically all micro-optimizations start at "look at the damn assembly" since people really suck at predicting what code will actually be generated.
Even with inlining the point is mostly the same - there aren't small program changes that lead to big changes in what is generated.
There are a lot of libraries written in C, like, thousands in Debian alone. You can use them from any language, but sometimes you need to write a little bit of glue C to get that to work. Sometimes it's less painful to just write your program in C++ or Objective-C so you don't have to debug your glue C.
If you want to write a library that can be used from any language, basically your options are C, C++ (but realistically its C subset), and Rust. Getting things to work all the time is easier in Rust than in C or C++. Getting things to work some of the time is easier in C.
But for most of these things it's probably adequate to be a mediocre C programmer. Unless your mediocrity is manifested in spending a week on tracking down a bug that should have taken an hour, maybe.
I think this is a good generalization of the learning curves of the languages, but not necessarily productivity for an experienced developer. Rust has modern amenities like pattern matching, generics and a package manager.
The biggest problem would likely bad code quality standards of earlier periods (lots of budding programmers in the dot com boom wrote a lot of poorly done code that survives today, which is responsible at least in part for Perls reputation as write only), but if you're just deploying your own code, that's less of an issue. Perl can be written to be readable and obvious, it just takes control. In that respect, I imagine it's a lot like C.
C is necessary in some paradigms/domains. But that list has been shrinking as other languages are born and mature.
Learning C, like learning most things, will still certainly lend itself to lots of things you do, even if you're not using C.
Why learn C? Because you want to do stuff on that list or because you feel like it. Why not learn something else instead? It may very well be that there's more worthwhile things to learn depending on your goals.
It is the most popular programming language. There is a lot of code written in it. Most jobs involve maintaining extant code. So it's good for that. It's safe to say there will be a need for C programmers for the next 100 years.
Why does the Rust community have to make every discussion about Rust?