Programming

Cache Lines Beat Clever Loops

I translated a matrix filter from C to Go and then spent an afternoon optimizing arithmetic that was not the main cost. The profile pointed at memory movement, so I reduced the problem to traversal order.

The matrix is stored in one slice in row-major order. These loops calculate the same sum:

for y := 0; y < height; y++ {
    for x := 0; x < width; x++ {
        sum += pixels[y*width+x]
    }
}
for x := 0; x < width; x++ {
    for y := 0; y < height; y++ {
        sum += pixels[y*width+x]
    }
}

The first walks adjacent elements. The second jumps by an entire row. On a large image the row-wise loop is much faster because each fetched cache line supplies several values used immediately. The column-wise loop asks the processor to fetch many lines and uses a small part of each before moving on.

Bounds checks and multiplication were my original suspects. Hoisting the row offset made the code clearer and helped a little:

for y := 0; y < height; y++ {
    row := pixels[y*width:(y+1)*width]
    for x := 0; x < width; x++ {
        sum += row[x]
    }
}

But traversal order remained the large effect. A more sophisticated arithmetic trick could not compensate for unfriendly access.

I benchmark with enough data to exceed the small caches, repeat the operation, and verify the sum so the compiler cannot discard the work. These are 2011 compiler snapshots, so absolute timings and optimization behavior are temporary facts. The memory hierarchy is less temporary.

I alternate test order as well. Running one version first every time can give it consistently colder data and turn benchmark order into a hidden input.

This also affects data structure choice. A slice of small structures may have better locality than a slice of pointers to separately allocated structures. The latter adds pointer chasing and gives the allocator freedom to scatter objects. It may still be right when identity and independent lifetimes matter, but it is not free.

My practical conclusion is embarrassingly physical: visit data in the order it lies in memory. Before unrolling loops or translating a bit trick from C, inspect the access pattern. The processor is very fast when fed and becomes an expensive space heater when asked to wait.

Replacing Signals With Small Interfaces

I ported the non-visual half of a Qt utility to Go and immediately missed signals and slots. They had been carrying progress, log messages, and completion notifications between objects. My first replacement was a channel for every event. It worked and looked like a telephone exchange designed by an enthusiastic octopus.

The simpler answer for synchronous notifications was a small interface:

type Progress interface {
    Step(done, total int)
}

func scan(files []string, progress Progress) os.Error {
    for i, name := range files {
        if err := scanFile(name); err != nil {
            return err
        }
        progress.Step(i+1, len(files))
    }
    return nil
}

The terminal implementation prints a line. A test implementation records calls. A future graphical shell can arrange delivery to its UI thread. The scanner knows only the behavior it needs.

This differs from a Qt signal in an important way: the call is ordinary and synchronous. The scanner waits while Step runs. That makes control flow and failure behavior obvious, but a slow observer slows the operation. If isolation is required, an adapter can implement Progress by sending values to a channel consumed elsewhere.

I prefer putting that asynchronous decision in the adapter rather than in the core interface. A channel everywhere turns every notification into a lifetime and shutdown problem. An interface everywhere can accidentally block. Neither mechanism removes design; they merely make different design visible.

For optional progress I use a no-op implementation rather than scattering nil checks through the scanner. It is one tiny type with one empty method, and it keeps the hot path unsurprising.

Completion remains a return, not another notification. The caller already has a direct control path, and duplicating it creates two accounts of whether work finished.

The Go compiler decides satisfaction structurally. My terminal reporter does not inherit from a framework base or declare that it implements Progress; matching Step is enough. That made extracting this contract from existing code much easier than I expected.

This is written against an early 2011 weekly snapshot, including the os.Error return convention. APIs will move. The lesson I am keeping is narrower: use a direct small interface for required behavior, then add a channel at the boundary where asynchronous delivery is actually needed. Not every callback needs its own postal service.

Merging Sorted Streams With Channels

I had several sorted result files and wanted one sorted stream without loading everything into memory. The classic algorithm is a k-way merge: keep the next item from each input, emit the smallest, then advance only that input.

My first Go version gave every file reader a goroutine and a channel. The merger holds one current value per channel:

type item struct {
    value int
    open  bool
}

func source(values []int) <-chan int {
    out := make(chan int)
    go func() {
        for _, value := range values {
            out <- value
        }
        close(out)
    }()
    return out
}

The merge loop initializes one item from every source, scans those current values for the smallest, emits it, and receives the replacement from the chosen source. When that receive reports the channel closed, the source leaves consideration. Memory remains proportional to the number of sources, apart from buffering in the file readers.

A linear scan makes each output operation O(k)O(k), where kk is the number of inputs. That is fine for four files and silly for four thousand. Replacing the scan with a binary heap makes selection O(logk)O(\log k). I implemented both and found the simple scan faster for my actual case of eight inputs. Algorithms still have constants, despite textbooks’ efforts to protect us from them.

Channels help isolate blocking input, but they do not improve the comparison algorithm. That distinction matters. Concurrency lets readers overlap waiting on disks or pipes; the heap changes CPU work. Combining both without measuring can add scheduling overhead to a problem that was already fast.

There is also a shutdown question. If the consumer stops early, source goroutines may remain blocked trying to send. My real version includes a separate stop channel selected by each producer, and the caller closes it when abandoning the merge. A finite example that always drains output can omit this, but reusable code cannot.

This code targets the January 2011 snapshot. Channel receive conventions and library calls should be checked against that compiler. The useful design survives those changes: one lookahead value per sorted source, explicit ownership of advancement, and a selection structure chosen for the actual number of streams.

The Slice Is Not the Array

Coming from C, I first read a Go slice as a pointer with nicer indexing. That model works until append allocates a new backing array or a tiny subslice keeps a large allocation alive. The missing piece is that a slice is a small descriptor, not the array itself.

Conceptually, the descriptor contains three items:

+---------+--------+----------+
| pointer | length | capacity |
+---------+--------+----------+
     |
     v
+----+----+----+----+----+
| 10 | 20 | 30 | 40 | 50 |
+----+----+----+----+----+

The pointer identifies an element in a backing array. Length controls the elements currently accessible through indexing. Capacity describes how far the slice may grow from that starting point before another array is needed. This is a conceptual layout, not a promise that user code should depend on runtime internals.

Given an array and a slice:

a := [5]int{10, 20, 30, 40, 50}
s := a[1:3]

fmt.Println(len(s), cap(s))

my snapshot prints:

2 4

The slice sees 20 and 30, and it has capacity through the end of a. Assigning s[0] = 99 changes a[1] because both views refer to the same storage. Passing s to a function copies the descriptor, but both descriptors still point at that storage.

This explains a function that appears not to resize its caller:

func grow(s []int) {
    s = append(s, 60)
}

The local descriptor changes. The caller’s descriptor does not. If the append fits within capacity, the backing array may contain the new element, but the caller’s length still excludes it. If it does not fit, append obtains another backing array and the local slice points there. The useful pattern is to return the result:

func grow(s []int) []int {
    return append(s, 60)
}

s = grow(s)

The same mechanism makes append performance less mysterious. Growing one element at a time does not normally allocate an array on every call. Capacity grows in larger steps and existing elements are copied when storage changes. The exact growth policy belongs to this experimental runtime and should not become an application assumption.

I verified allocations indirectly by timing construction with and without an initial capacity:

a := make([]int, 0, 100000)
for i := 0; i < 100000; i++ {
    a = append(a, i)
}

When the final size is known, reserving capacity avoids repeated growth and copying. When it is not known, append’s policy is generally better than inventing my own allocation scheme. Capacity is a performance hint with semantic consequences, not a target to maximize.

Subslice lifetime is the less obvious consequence. I wrote a reader that loaded a large file, found one short token, and returned a slice containing that token. The token was only a few bytes, but its descriptor still pointed into the large backing array. As long as the small slice remained reachable, the collector could not reclaim the array.

The fix was to copy the useful bytes into a new, correctly sized slice:

token := make([]byte, end-start)
copy(token, data[start:end])
return token

Copying sounds wasteful until the alternative is retaining several megabytes for a twelve-byte identifier. Profiling the whole process settled that argument quickly.

Overlapping slices deserve similar care. copy is the operation intended for moving slice contents, including overlap supported by the implementation. Hand-written forward loops can overwrite input before reading it. This is one of those cases where code that looks closer to C is merely closer to a C bug.

Slices also make zero values useful. A nil slice has length and capacity zero, can be ranged over, and can be appended to. For many producers there is no need to allocate an empty slice eagerly. An empty non-nil slice may still matter at an encoding boundary, but inside the program I prefer the simpler zero value unless behavior says otherwise.

Arrays remain values with their length in the type. Assigning an array copies its elements, and [4]int differs from [5]int. Slices provide the flexible view normally wanted by functions. Remembering that distinction prevents a great deal of accidental copying and aliasing.

All of this describes the November 2010 toolchain I am using. Syntax, built-ins, and growth details may change before a stable release. The useful mental model is simpler: a slice is a copied descriptor over shared storage. Ask which descriptor changes, which array it references, and who keeps that array alive. That usually finds the surprise.

Reflection With the Lights On

I wanted a diagnostic printer for configuration structures and reached for reflection. That is normally the moment a small helper begins applying for framework status, so I kept the experiment deliberately narrow.

An interface value carries a dynamic type and value. The reflect package exposes descriptions of those two parts. In my weekly snapshot the entry points and names differ from examples written against other snapshots, so I will not pretend this code is timeless:

v := reflect.NewValue(x)
t := reflect.Typeof(x)
fmt.Println(t.String(), v.Kind())

From there I can switch on kind, inspect structure fields, and recursively print supported values. The mechanism is not source-code introspection. It is runtime examination of type information retained for a value.

The first rule I learned is to separate validity, kind, and concrete type. A pointer is not its element. An interface can contain a pointer. A nil interface and an interface containing a nil pointer are different states. Reflection faithfully presents these distinctions, even when I would prefer it to edit my mistake.

The second rule is that settable values are special. Inspecting a copy obtained from an interface does not grant permission to modify the original. To change a caller’s value, reflection needs an addressable value, usually reached through a pointer and then its element. This is good friction. Generic mutation should look dangerous because it is.

I ended with a read-only printer supporting strings, integers, slices, and exported structure fields. Unsupported kinds produce an explicit marker. I did not add automatic conversion, tag syntax, or clever cycle handling because the tool does not need them.

Reflection trades compiler knowledge for runtime decisions. Misspelled field assumptions become execution-path problems, and every branch is harder to read than direct code. It is justified when the types truly vary, as in encoders and diagnostics. It is not justified merely to avoid writing three straightforward assignments.

My opinion after using it is positive but cautious. Go’s reflection model follows the interface representation and type system rather than inventing a parallel object universe. Still, if ordinary interfaces can express the job, they are clearer and checked earlier. Reflection is a sharp tool. Keeping the lights on means seeing both the blade and the fingers.