A Boggling Return to C
thraxil.org
thraxil.org
C is the sweet spot where I can extend my programs to do high-level stuff while still keep my hands down on the actual hardware I'm programming. I like that a lot, probably because I grew up banging hardware. Or maybe it is because I like to keep in touch with the actual device that I program: bending some definitions, C is what my machine does. Even if I embed Lua and script parts of my program, I'm still conceptually working on a C runtime, complete with addresses, pointers, integers and registers. That's why I also like LLVM as it abstracts away different instruction sets into a generic high-level instruction set, much like C abstracts different assembly languages into a generic high-level assembly language.
C also has the property of being most enjoyable code to read. I've spent a lot of time just reading C out of sheer enjoyment. C is tricky: it can be a total mess or it can be terse and beautiful and clear. No matter what, it describes exactly what it does to my machine. Read some of D.J. Bernsteins source trees to get an idea of how neat C can be.
The source to qmail/daemontools is a pleasure to read though and having read it I feel like I have learned a lot from what it has to offer.
Arthur Whitney is another I would put on par with djb. He's a bit older than djb.
For expertise you can't beat W. Richard Stevens. He also studies and wrote about FORTH before focusing on solely on C.
Both Whitney and djb have a true appreciation for speed, efficiency and succinctness; both have solid foundations in maths; both can build very level abstractions. But they have different areas of focus.
djb - secure systems administration and networking. (Stevens - documentation.) Whitney - Lisp background; big data.
Whitney has proven that it's possible, using a matrix-based approach, to meet or beat the speed of C with an interpreted language.
But it's difficult to write UNIX systems or networking code without knowing C. For guidance on navigating the many pitfalls of C, djb and Stevens are as good as it gets.
One thing I realized, is that reading C is more tedious than code in other languages. Sure that's a gross generalization and is not true for every piece of code out there. However, I find I have less troubles picking up a Python project, understanding how it's written and start contributing than I have with C.
A few weeks ago, I was looking at the code of Qemu. The code relies heavily on preprocessor macros and some weird gcc-only syntax that made my head hurt. It was difficult.
I guess what I'm trying to say, my only problem with C is that it doesn't force the programmer to write in a clear understandable way. Or maybe that's just me.
(Make is another tool which suffers from 'sour spot' syndrome IMHO.)
The preprocessor is generally recognized as one of C's biggest flaws, is not for nothing that Ken Thompson, the first C programmer and the greatest influence on the language other than DMR himself, cut down most of the preprocessor when he wrote his own set of C compilers for Plan 9: http://doc.cat-v.org/plan_9/4th_edition/papers/comp
And of course Go has no preprocessor.
As for your second complaint, one can't blame C for gcc's extensions ;)
You should try Go, many people would consider it C's spiritual successor (or as somebody put it: the language the people who created C would come up if they had 40 years to think about how to polish and improve it), it keeps all the simplicity of C, while being probably the most readable language I have used, it is concise but keeps everything explicit, and figuring what code does is very easy, because code does what it says and says what it does, no dark magic needed.
You cannot easily write device drivers for existing operating systems in Go because the existing operating systems provide a particular environment unsuited for Go and expect certain constraints from the device drivers themselves, constraints which Go breaks. In principle, it could be made to work.
from somedir.somefile import objectX
Then when you go to somedir.somefile you find out objectX is nowhere to be found only to figure out later that it is dynamically created and added to that namespace and it can be imported in the file you've been reading because some init function was already called by the module "foo".There are quite a few codebases where I have been hunting for the superclass for example so that I could see if it offered functionality I wanted or how it was structured so I could find out where some function was defined and what EXACTLY it did due to no documentation and it took me a while.
I love Python, don't get me wrong. It is by far one of my favourite programming languages, but sometimes it can be very non-obvious where something is coming from and how it is getting there. This may be more of an issue on a project to project basis, but it is an extra complexity that I have found can be rather annoying.
The inspect module doc page is of great help: http://docs.python.org/library/inspect.html
In this respect method_missing? is analogous to (over)using C's preprocessors. It seemed like a good idea at the time, but...
> method(:gem).source_location
=> ["/Users/wycats/.rvm/rubies/ruby-1.9.3-p194/lib/ruby/site_ruby/1.9.1/rubygems.rb", 1228]Some people actually had the gall to complain about him ensuring the noobies learned to use valgrind before proceeding to write any real code.
Ingrateful dipshits don't remember what the pre-valgrind/dtrace days were like.
Last time I wrote anything in C was on Netware NLMs, and it has been long enough that I have mostly forgotten what I knew.
i don't know if you know the book - an older copy is on my desk and it use it regularly when working in c - but it's part introduction and part informed guide to the libraries. it's not a "friendly" book (it's not for "dummies"), but it's well written and surprisingly compact for all it contains (at least, the copy i have is; i am waiting for delivery of the latest version).
If you don't believe me, just run Valgrind on random sampling of C programs that didn't use such a tool - I reckon it will find issues with most of them.
I do not necessarily abide by this point of view, but I do respect the kind of harsh discipline it advocates. Valgrind is definitely useful, really useful, but great C programs (and programmers) existed long before it appeared.
(valgrind's lack of availability is a bit of a problem. I'm not complaining - I bet it is a bit fiddly to port it to a new system - but it's very easy to never have come across any system that can run it in your professional life. So being able to work without it is no waste of time.)
Uninitialised data and memory scribbles can be tricky to detect 100% reliably without valgrind, but if you code appropriately, you'll spot it. Leaks are very easy to find (fixing them, not always so much), and I don't really understand why one needs this monster program to discover them - but maybe one day I'll actually be in a position to use it, and I'll find out what I'm missing.
C made lots of sense in the context it was developed, but the world would be better if we had safer systems programming languages.
Valgrind, purify and friends are required to C, the same way Java requires IDEs, to improve language usability.
Have said this, the new trend in having static analysis tools integrated in the development process, like Clang, Eclipse CODA, Visual Studio's tools or HP Code Advisor, among others, can bring a bit more safety into C.
We have "safer" languages but our systems and tools remain in C. Given the intense competition in software, there must be solid reasons C remains the foundation of computing.
"Skilled C programmers do not need profilers, bounds checkers and memory leak detectors."
Skilled programmers use profilers because the alternative (guessing) is a poor strategy for diagnosing poor performance. And no skilled programmer would spurn a useful tool like a bounds checker or memory leak detector, because nobody is perfect and these tools save immense amounts of time by pointing you straight to the problem.
Programmers who don't use profilers write slow programs (even though they spend many hours "optimizing" them).
Sadly you don't find many of them in the enterprise world, specially when dealing with off-shoring companies. :(
> ... Given the intense competition in software, there must be solid reasons C remains the foundation of computing.
Because it is dumb to code everything new, just because another language is cooler, nicer, safer, etc. So existing software keeps being coded in C.
Even if I expressed my opinion the way I did, I will surely pick C if it makes sense for the project at hand.
- Donald Knuth (wrote a study on profilers)
- Rob Pike (wrote a profiler, 'though for FORTRAN)
- Brian Kernighan (used a profiler to double the speed of his AWK interpreter)
I wonder who you consider a skilled C programmer, if not K from K&R.
Why attack C at all? Why not instead convince us of the merits of another language?
There are great tools around. I agree with that. Which parts of Clang do you like best?
The re-factoring support and modules extensions are also a welcome additions.
And that's without the commercial tools such as Coverity.
I also love the idea of using a trie here. That's something else I've been wanting to play around with for a while. Although now I'd do it in a functional language.
He brings up a good point: people coding in C tend to stop and think about data structures, memory usage, and clock cycles in a way people using higher languages very rarely do. It's part of the way to "think in C" Internal data structures are also much more important in FP. Interesting how different languages cause you to think in different ways. (Sapir-Worf anyone?) :)
I started programming in C/C++ (my first book, I'll admit at age 9 was a C++ for Dummies book, it came with a compiler :P).
I have learned and use a lot of higher level languages, but I still think about data structures, I still think about what the best way would be for handling the data most efficiently, mainly because I don't want to rely on the language doing the right thing.
I've seen Java programmers though that then start programming in C++ or even C and never pick up the art of thinking about their data structures. I work with one co-worker now that went through the extreme trouble of implementing Java like enum's which have caused all kinds of "warts" and all kinds of issues because they are not enums and they aren't "real" classes.
Watching Java developers turn C++/C developers is really interesting, they bring all kinds of "bad" practices back with them and the code is worse off because of it!
Only tenfold? Interesting. While Python is surely not the slowest interpreted language around, a result like that borders on the performance of Java. That seems unlikely, especially given the fact that Python version uses worse algorithm.
I would think about how big is the portion of time eaten by I/O - that is, actually reading the `words` file from disk. I wouldn't be surprised if it eats most of the ~100ms that C needs to performs the task, leaving only a tiny percent for actual computation.
Ultimately though, this is still why Python gets used so much for real world work. Slow and inefficient as it might be compared to C, on real world problems where performance is dominated by disk seeks and network latency, it's good enough.
What Java program and timings are you looking at?
I haven't seen the "Java is slow" chestnut in years.
The point is, if you assign nearly mystical properties to writing in C, but when you rewrite a brute force approach Python program with a much fancier algorithm in C and you "only" get 10x speedup then something is amiss.
I know a programmer who's been working in the industry since the 70s, she did the same damn thing when she was young with the same exact data structure.
from Bio.trie import trieI wrote the Python version in probably under an hour. My girlfriend had gotten into playing some stupid Facebook version of Boggle and I just wanted to see her face when I came out of nowhere with implausibly high scores. I didn't think hard about the problem, just reached for the tool I know best and implemented the first obvious approach that came to mind. It worked as needed and I moved on. You make it sound like I think that's a bad thing.
Later, when I had a bit of time to think about it, it occurred to me that a trie would be a better approach, so when I was feeling like getting back into C and wanted a toy problem, re-implementing the boggle solver in C with a trie seemed like a good choice.
The experience of programming in the different languages does feel different though and I think it can affect how one approaches solving problems. Python is so good at just letting me solve the immediate problem that sometimes I rush and don't think things through or settle for a less than optimal solution. This will come back and bite me if that suboptimal code ends up getting built on and re-used elsewhere.
When I write C (or Go, Erlang, Haskell, etc. basically any language that requires me to think a little more up-front about how I'll implement it), I know going in that I'm going to be putting some serious time and effort into the code, so I tend to be more careful about things at every stage. The game changes from "get a result as quickly and painlessly as possible" to "write something that is elegant in itself". That's not always a win. Sometimes you are much better off building the prototype quickly, seeing flaws that you never would've thought of and then being able to approach the problem in a whole new way. Sometimes you just need a result quickly and time spent making things elegant or efficient actually is wasted (I'm not going to build a framework out of the boggle solving code anytime soon, eg).
I code in Python pretty much every day. I have for years. I probably will for years to come. It works for me. I'm just saying that sometimes other languages push you in different directions and I can see why, despite taking more lines of code to write, taking longer to write, having more potential for segfaults, and so on, languages like C still find a niche for writing systems and platforms. And that reason isn't just that it runs a little faster.
Edit: The next Python answer there uses a trie and takes 16.7 seconds on the 4x4 board. I like tries because they're elegant, but I hardly ever use them because the built-in collections are well-engineered even for problems you'd think are made for a trie.
Oh boy. This looks like the type of C code I write. Could some experienced C programmer please point out what parts are cringe inducing ?
struct foo f() {
struct foo f;
return f;
}
isn't returning a stack-allocated struct a bad idea?#include <stdio.h>
struct foo { char space[1024]; };
struct foo f()
{
struct foo f;
printf("address of f is %p\n",&f);
return f;
}int main()
{
struct foo g;
g = f();
printf("address of g is %p\n",&g);
return 0;
}produces this output:
address of f is 0x7fbfffec30
address of g is 0x7fbffff050
The trie code as well as the display UI were provided - you only had to write the board-walking code.
Being as this was the first time I had ever written any program, I remember it being quite challenging but also really fun. It was great to see your own program utterly house you when you played against it.
I wonder if I still have that code somewhere ... it would be fun to look at / cringe.
Hope it will make me a better programmer.
C: A Reference Manual is excellent and is probably the only other C book you need, but only if you are already a C programmer, and only for what the title implies: reference.
Someone on this forum recommended it to me. It's been invaluable assisting me in refactoring major portions of a legacy code base. In a way, it's helped me bring the DRY principle to our C application. I write in C every single day though and it may not be applicable for a hobbyist/generalist.