Computational Literacies Lab

0002.o Performance, readability, and documentation

On this page
  1. Writing for readers
  2. Refactoring
  3. Writing for the computer

Code has two audiences. The computer executes it, and the qualities that matter to the computer are correctness and speed. People read it, whether teammates, a teacher, or the original author months later, and what matters to them is whether they can understand it and change it safely. This section is organized around those two audiences. It begins with the practices that serve human readers, which are naming, comments and documentation, and named constants. It then considers refactoring, which improves structure for readers while leaving the computer's view unchanged. It ends with performance, where the main lesson is that the biggest gains come from choosing the right structures and algorithms, which seldom requires sacrificing readability.

Writing for readers#

Names are the first thing a reader sees. Descriptive names such as count_passing, scores, and cutoff communicate a variable's purpose, so a reader doesn't have to work it out from how the variable is used, and a function named has_negative tells a caller what it reports before they read a line of its body. Names don't affect what a program does or how fast it runs. Two versions of a program that differ only in their names produce identical output, and one of them is simply easier to read.1 Consistent conventions matter as well. A project in which some functions are called get_total and others CalcSum and fetchAverage makes a reader stop and wonder whether the differences mean something, whereas a shared convention makes code predictable.

Names say what something is, and comments can say what names can't. Good comments explain why code exists, rather than restating what it does, since the code already shows what it does. Above rate = 0.0725, a comment such as /* set the rate */ merely restates the line, while /* state sales tax rate for 2031; update each January */ explains the value's meaning and origin and tells a future reader when it needs attention. Comments carry a risk that code doesn't: they aren't checked by the computer, so when code changes and its comment doesn't, the comment misleads. A function whose comment says it returns dollars, after a later change made it return cents, is worse off than one with no comment at all.

Documentation serves a different reader, the person who uses a program or function without reading its inside. It describes how to use it: its purpose, its inputs or parameters, its outputs or return value, and any errors it can produce. A programmer publishing a function for others should document exactly these things, since callers need to know how to use it, not how it works internally. This is the interface of 0002.e and 0002.l, put into words.

Numbers deserve the same care as names. A magic number is an unexplained number in code, such as 86400 appearing in several calculations. Replacing it with a named constant such as SECONDS_PER_DAY states its meaning, and it means the value can be changed in one location. Values that happen to be equal still need separate names when they mean different things. If a game uses 3 for both the number of lives and the number of levels, a designer who changes lives to 5 with a global search and replace will change the levels too, and separate constants for lives and levels would have prevented it. Judgment still applies, and a rule against all bare numbers needs exceptions: starting an index at 0 in a loop is clear without a name.

Refactoring#

The practices above can be applied while code is written, but code also needs improving after the fact. Refactoring means changing a program's internal structure to improve readability or maintainability without changing its behavior. Extracting a repeated block into a function, splitting a long function into three smaller ones, or removing a statement that appears in both branches of an if are all refactorings:

01 if (paid)
02   output("ok")
03   status = "member"
04 else
05   output("ok")
06   status = "guest"

Since output("ok") runs first in both branches, it can move to a single line before the if, and the program behaves exactly as before. That condition, unchanged behavior, is what defines refactoring, so fixing a bug or adding a feature isn't refactoring, even when it is done at the same time. Tests provide the evidence that behavior hasn't changed: if all tests pass with the same outputs after restructuring, the refactoring has preserved behavior as far as the tests can tell. This is why the safest first step in refactoring a function with no tests is to write tests that capture its current behavior.

Writing for the computer#

Readability and performance are often presented as opposites, but most performance gains come from changes that also keep code clear. One of the simplest is to move unchanging work out of loops. A value that doesn't change from one pass to the next should be computed once, before the loop. A loop's condition is evaluated before every pass, so an expensive call placed in the condition repeats on every pass as well. The effect can change an algorithm's growth class:

01 int count = 0
02 for (int i = 0; i < s.size[1]; i = i + 1)
03   if (s[i] > average(s))
04     count = count + 1

If average(s) loops over the whole array, calling it on every pass makes this O(n²). Since the average doesn't depend on i, computing it once before the loop, as int avg = average(s), and comparing with avg makes it O(n) with the same output. Similarly, a loop that calls Math.sqrt(area) on each iteration, when area doesn't change, should store the square root in a variable first. The restriction is essential: only work that truly doesn't change can be moved. If an expression depends on a value that changes inside the loop, moving it out produces wrong results.

The largest gains come from a higher level. Choosing a suitable data structure or algorithm generally improves performance far more than small adjustments to individual lines, because it changes the growth class, the subject of 0002.k. A school program that scans an array of records from the beginning for each of thousands of daily lookups by ID becomes much faster with a map keyed by student ID, since key lookup is typically close to constant time. A program checking whether each new name has been seen before benefits in the same way from a map with names as keys. A sorted list of a million values used for many lookups gains far more from switching to binary search, from O(n) to O(log n), than from rewriting its linear search in faster-looking code. Removing comments or shortening names, by contrast, does nothing for speed.

Before optimizing anything, measure, to find out where the time actually goes. Effort spent on a part of a program that rarely runs is wasted, and a 2% gain from careful micro-optimizations that make a loop much harder to read is usually a bad trade. Measuring first directs effort to where it matters, and usually that turns out to be a choice of structure that serves both audiences at once.

Readable, well-structured code matters most when several people work on it, and the tools that support that work are the subject of the final section of this competency.

  • descriptive name: a variable or function name that communicates its purpose
  • comment: a note in code, best used to explain why the code exists
  • documentation: a description of how to use a program or function, including its purpose, inputs, outputs, and errors
  • magic number: an unexplained number in code
  • named constant: a named value, such as SECONDS_PER_DAY, that replaces a magic number
  • refactoring: changing internal structure to improve readability or maintainability without changing behavior

Practice: practice questions for 0002.o

  1. MWC1, Names: variables, values, and functions with arguments. ↩