Sometimes we have huge code listings to deal with, and I'm curious as to how the advanced, highly skilled programmers read code. And I'll say with an emphasis on debugging and finding issues.
Depending on what you need out of the code why read it all. Searching by the words you need lets you jump to the parts you actually need so you can read smaller chunks.
Most modern IDEs I've used lately let you do some flavor of a project or directory wide search for a given term which allows you to jump between references even if you don't really know where to look for them. If it doesn't there are often ways to accomplish a similar task through your operating system. Some (like visual studio) will even let you jump to the definition of a given variable or function so you can see what is actually happening in a given call or assignment.
Depending on your language you are using you may even have a reasonable stacktrace to trace through the code which eliminates the need to search and sort through miscellaneous things. You said your focus is on debugging...if you are using a debugger in many languages this will severely reduce the amount of code you actually have to read. It will help you walk line by line through the fault so you can carefully examine the code that matters while doing it in less time than reading bulks of code.
The key isn't to read it fast. It's to filter out the noise so you can carefully read what is important.
Now, if you are reading to understand the architecture or functionality of a system you shouldn't try to speed read. You should be reading everything intently. Then again, you shouldn't have to do this in a perfect world because the code would be documented.
Reading a large project is an iterative process.
First, you try to get an overview of the structure. Skim through the documentation, look through the directory structure, check out the build organization, that kind of thing. Try to get at least a cursory idea what files and directory trees are for.
Then, try to pick out the important elements. As above, don't worry if you don't have 100% comprehension of what you're looking at -- you're going to go over it again anyway. It helps if you are looking for a particular thing (e.g., you want to debug a particular problem, or you want to adapt a particular module for your purposes), since that allows you to cut down the information that needs to be filtered and absorbed. It also helps if your codebase is well-designed. But the process is the same in any case.
Once you have found something that seems relevant, read (or reread) through it, and search out connections in the rest of the project. As other answers indicate, find/grep or IDE search is a useful tool here.
Then, iterate: read or re-read code you have found relevant. Prune connections that aren't immediately relevant, and check out connections that might be. As you gain a better understanding of the structure and logic of the codebase, it will be easier to understand the details of what you read. Eventually, you will become enlightened...
This may not sound particularly like "speed-reading", but it is the fastest way to gain the understanding you need to solve your problem. The skimming part basically is speed-reading, but I think the pruning and iteration parts are more important for reducing time-to-comprehension.
Leverage your IDE. Use search, go-to declaration, outline-view, maybe even fancy visualization tools.
Setup the code you are debugging in a test fixture if possible. Setup your IDE so you can run the test with one hotkey. Run the code over and over while inserting logging statements and manipulating the code to see how inputs affect the outputs.
Speed reading code comes with practice, just by reading lots of code in that language, in that style, in that codebase. It is for when all other methods of finding the code you are interested in fail, because it is one of the slowest.
Why are you reading this code?
We don't get paid to read code, we get paid to deliver features. If I have to read a lot of code in pursuit of some customer requirement, then there is missing documentation. I add the documentation as I read. That keeps me focused, and saves the next person a lot of time.
If I am finding bugs as I read the code, then I will record them in the issue tracking system and/or insert to-do markers in the code. If there are unit tests, then I will refactor the code as I go.
If the job is to add features by making minimal changes in a manually tested big-ball-of-mud, it won't be me doing it.
A simple way to scan code for problems is to watch indentation and method length; if you're indented too deep, you're probably nesting too deep; this is symptomatic of overly nested
If a method is too long, odds are something is wrong with it. I've seen cases where it's really two methods with a giant
Use your IDE and debuggers. Read and fix IDE warnings. I'm amazed how many old projects I open from other programmers and right there are warnings that haven't been addressed. Code correctness is important even if your code usually works at runtime.
Focus on finding issues, not reading everything. Locate Code Smells in otherwise working systems to help get you started. Once you find one problem with someone else's code it will often help you find others. Once you find one God Object in a programmer's work, chances are many or all of their projects have them.
Either by adjusting the font size or using the zoom feature on your IDE you can zoom out on a page of code until you can only see the jaggedness of indention.
In one popular style of unit test writing you'd expect to see unit test code with very little jaggedness of indention (few nests or loops)
On code under test, you'd expect to see nests and loops. In possibly overly complicated code, you'd see very deep jags, showing excess nesting and looping happening all in the same place and time.
There are a number of speed-reading techniques, but the essence is represented in the concepts of chunking and skimming. Both of these techniques translate well into code, and both tend to develop naturally in people who read lots of code. But in debugging, speed reading tends to be the opposite of what you want to do. Let me demonstrate:
Glance for a moment at the following pattern. Not long, just long enough to see what it is.
Without even reading the whole thing, you almost certainly immediately recognized it as a class definition. You saw
But by ignoring irrelevant details, you tend to miss important ones. If you had really been carefully reading the code, you probably would have gotten hung up on the fact that it has a glaring error that would prevent it from compiling or running... whatever language it represents. You missed it? Had you been reading out loud: "void hello return forty-two" rather than just looking at the shape, it would almost certainly have caused you to pause.
Which is why the most important part about speed reading is understanding its place. You zip through code when you're trying to identify something. You look at shapes and key words and completely ignore entire chunks of irrelevant details. That's speed reading.
But when you've found something you think might be wrong, you have to stop speed reading. You slow down and force your mind to linger long enough on a pattern to see the error -- a thing that does not come naturally and has to be forced. You read it out loud if it helps. You stare at it looking for spelling mistakes and logic failures. You do what is the exact antithesis of speed reading.
Speed reading comes all too naturally to the seasoned programmer because his mind is shaped around identifying patterns in code. But when you're debugging, the patterns aren't generally the critical features, instead the details are.
On the quality of the code. If the code is written with a lot of convention, clarity, and safety, then sure, you can zoom through the code. But if it was written sloppily and unclearly, that's a bag of problems you have to sort through.
I don't think code is something that you can speed read through unless it is good code. In Good Code, behaviors are well scoped and are tucked beneath properly named methods. In Bad Code, there could be far reaching side effects and misleading punctuation.
I think, that reading the whole huge listing is not always a good way to find and fix the problem. When I have to find a reason of a wrong behavior in the program, I use the divide-and-conquer strategy. It's very important to be able to make good assumptions about where to start your search from. As other guys have said, there are a lot of useful techniques you can use like unit-tests, full text search, symbol search and so on.
Another important things are patience, slow pace and deliberation. It may sound funny but being meticulous when you fill it is required usually helps to find bug faster. And the last but not least... make less assumptions and more tests, experiment with the code to prove your thoughts.
If you know what you are looking for: (like a variable, some reference or an issue)
Lifetime of data:
Many times, it is useful to see where the life of a variable begins, what stages it passes through and where it ends. Along the path, you'll know where it is changes, how it alters the flow of program, etc.
Real ife methods could be ugly. 500 lines of code in a single method isn't easy to read. It need multiple passes. With each pass you taking into account: larger sections of code, loops, conditions; one by one. If the method needs to be understood well, I often write pseudo-code corresponding to the method. Pseudo-code is short, and since, you write it yourself, you'll understand it well.
The line between reading code and writing code is thin. Once, you have understood the code you are reading. If there is a better way of arranging it, you should do it. (There is too much bad code in the world).
If you need to understand the architecture / big picture:
Pay attention to architectural elements: like, interfaces, classes, their important methods. Avoid the temptation to get into details. Form a mental picture of how major components are arranged and how they interact with each other.
People above have made the main good suggestions I think, but I wanted to add another thought that nobody mentioned.
You didn't mention language at all, but if it's a language that Doxygen supports, then you can use Doxygen to do some neat stuff. Even if zero explicit Doxygen style documentation has been done, Doxygen can generate inheritance graphs, include graphs, and class dependence graphs. You can even click around to move around through them. This lets you pick something you want to understand, and isolate the part of the codebase that is necessary to understanding that part, and repeat.
A lot of the time with big codebases I think the order in which you try to start reading the code is very important, not less important than how you actually go about trying to read the code.
I tend to navigate unfamiliar codebases by find-in-files for keywords I suspect will occur in the area of code i'm trying to get to.
You have reproduced a bug in which some text appears offset from where it should be. Hm, is that text a literal.. maybe i can find it that way.. nope, hm I bet that part of the string is a literal though, cool found a strings.resource for it, now who references that... hm, ok found who is doing the rendering, now who sets the location ... etc
Once I have a starting point found via find-in-files, I can navigate the rest of the codebase towards what I am trying to discover by following references to symbols.