Consider the layers of software abstraction counting upwards from 0 (the machine code) to n (whatever we’re working with now).
As we add more layers on top, layer zero becomes more and more irrelevant. However, that doesn’t mean that n-1, n-2, n-3 and so on are irrelevant. If our languages and tools are leaky abstractions, what we need is some kind of uniform depth, and the problem with JavaSchools is that they teach a very thin approach to programming.
It’s true that with new languages and tools and so on the specific layers Joel is talking in about become irrelevant, but we could write this post today about JavaScript programmers who don’t understand the fundamentals about the Java object model or Ruby programmers who don’t grok how Modules or Lambdas really work because they’ve been spoon-fed Rails.
A Ruby programmer may not need layers 0, 1, or 2 but I bet he does need R (Rails), as well as R-1, R-2, R-3, and R-4 whatever they may be.
I've encountered programmers in high-level languages that have some minimum level of figuring-things-out where they just give up and go "I guess (Rails|Ruby|the web server|the compiler|the VM|the OS|my hardware) just does X" at times when someone really, really needs to understand X because it's deadly important. (You could probably make an analogous argument for logical and mathematical fundamentals.) For a lot of programmers, the minimum level is completely unacceptable -- it's something like "the behavior is in code I didn't write" and they stop trying to understand once that barrier is crossed.
I think the easiest way to train people away from that is just by giving them a basic understanding of hardware, assembly language, C, compilers, and operating systems, even if most of that knowledge is only directly useful now and then.
This is just not true for performance-critical code. I've seen huge swings in performance from hand-arranging code to achieve better instruction-level parallelism, less loop overhead, etc. Not an order of magnitude but 3-5x routinely.
This is not worth doing for random bits of code outside serious hot spots, mind you, and it's become quite difficult to guess how a modern processor (e.g. Core 2 x86 or beyond) is going to execute instructions. Profiling with event counters as well as using IACA (Intel's static analyzer) becomes necessary, as does close reading of their optimization reference manual.
All of these concepts and tricks of the old programming days are still important, at the same time with larger systems and changing nature of software development we need to start teaching the hard lessons about large distributed system design, and this is the area I suspect schools are failing to provide for.
One could set the same bar during an interview by asking questions requiring the skilled use of pointers and recursion, and most Javaschool graduates without other programming experience will fail. This, of course has a high rate of false positives since such people have never attempted to learn pointers and recursion and therefore won't likely get it fast enough to answer an interview question.
Not understanding the underlying fundamentals when programming in high level languages can be really dangerous, but generally it's not. I kind of view the ongoing development of Compilers, VMs, JITs, etc. as being somewhat parallel to that of processors. At one point, optimizing with machine code was crucial to obtain performant code, but with crazy-ridiculous superscalar processors and all of the things that come with it, all you can really do is code C, compile with -O2, and then guess and check on optimizations. I wouldn't be surprised if this was the case in most high-level languages before long.
Don't get me wrong, I first read this in High School and this was the essay that made me love C, and made me want to try to get an internship at Fog Creek. I just think it's starting to show its age.