Programming

Go Is an Interesting Experiment

Google announced a new language called Go last week, and I spent an evening rewriting a small concurrent program in it. This is not a review. Go is new and experimental, its tools are young, and its APIs are changing. Anything written today may become an historical document by next month.

The appealing part is its systems-shaped simplicity. It has compiled code, pointers and familiar control structures, but also garbage collection, interfaces, goroutines and channels. Starting concurrent work is cheap in syntax, while channels provide a way to pass values instead of sharing every structure behind a lock.

My first version translated threads directly into goroutines and retained all the old shared state. Unsurprisingly, new spelling did not improve the design. The better version gave one goroutine ownership of the state and sent it requests through channels. The useful idea was ownership, not the keyword.

There are rough edges, incomplete libraries and unanswered questions about performance and deployment. I would not build an important product around today’s interfaces. I would use it for experiments, particularly network services where its concurrency model can be exercised honestly.

I strongly like the direction, with emphasis on direction. Go is not yet a settled platform, and confidence would be silly after one week. Still, it has made me think differently about a program I already understood, which is a fine result for an evening and cheaper than another programming book.

Backpressure Is Not an Error Message

My event-driven server stayed responsive under load, but its memory use climbed steadily. I had removed blocking and accidentally built an extremely efficient machine for accepting work it could not finish.

Each connection had an output buffer. Producers appended responses faster than the network drained them, so those buffers became an unbounded queue distributed across clients. Nothing was technically stuck. The server was merely saving enough unfinished work to become stuck later.

Backpressure means carrying limited downstream capacity toward the producer. When a connection’s output crosses a high-water mark, I stop reading more requests from it or stop scheduling new work. Once buffered output falls below a lower mark, reading resumes. Separate thresholds avoid switching state on every small write.

The same rule applies between the event loop and worker pool. A bounded job queue makes overload visible. If it is full, the loop must defer input, reject work or shed a connection according to policy. Adding another unbounded queue does not increase capacity; it increases the delay before admitting there is none.

I prefer bounded queues and explicit overload behavior, even when rejection feels impolite. The exact limits require measurement, and short bursts deserve some room. But a service that says “not now” can recover. One that accepts everything may respond to nobody, which is very accommodating in principle and less so in practice.

Qt 4.5 Lowers Two Barriers

Two things have repeatedly made Qt harder to suggest: licensing confusion and the lack of an obvious first development environment. Qt 4.5 improved both.

Qt is now available under the LGPL as well as its existing licenses. That gives more applications a straightforward way to use the library while keeping their own licensing choices, provided they comply with the LGPL’s requirements. It is not a declaration that licenses no longer need reading. It merely makes the common case less theatrical.

Qt Creator 1.0 supplies the other missing front door. It understands Qt projects, code navigation, building and debugging without trying to become an operating system disguised as an editor. I opened an existing project and was productive quickly, despite initially searching for several commands where my usual editor keeps them.

Creator is still young, and experienced users with carefully constructed environments may not gain much. I am not abandoning my editor after one pleasant afternoon. For newcomers, however, a focused tool removes a pile of setup from the first experiment.

I strongly prefer this combination: a capable cross-platform toolkit with a less restrictive entry point and an IDE that teaches its normal workflow. Neither guarantees good applications. They simply let people reach the part where application design can go wrong, which is where programmers traditionally demonstrate real creativity.

The Allocator Is Part of the Program

A multithreaded parser scaled nicely to two workers and then barely improved. I inspected locks in my code and found no obvious villain. Eventually a profile pointed at allocation and deallocation, which felt unfair because malloc() had not appeared on my list of interesting algorithms.

An allocator manages more than a pointer advancing through memory. It finds suitably sized free regions, records metadata, returns memory to reusable pools and sometimes asks the operating system for more pages. In multiple threads it must also protect shared structures. A program creating millions of short-lived objects can therefore spend substantial time contending inside code it never explicitly called.

The system allocator already uses techniques to reduce this cost. Implementations may maintain size classes, bins and multiple arenas so not every allocation takes one global lock. Alternative allocators such as tcmalloc and jemalloc make different tradeoffs involving per-thread caches, fragmentation and concurrency. Swapping one in can be an excellent experiment, but it is not a substitute for understanding the allocation pattern.

My parser allocated a small object for every token, then freed the whole tree after processing a request. The lifetimes were almost identical. A simple region allocator fit better: reserve larger blocks, hand out aligned pieces by moving a pointer, and release the region at once when the request completes. Individual deallocation disappears because the ownership model says nothing outlives the request.

This improved both speed and clarity. The gain did not come from a magical allocator; it came from expressing lifetime in the data structure. It also introduced a strict rule: pointers into the region cannot escape. Violating that rule turns cleanup into a mass dangling-pointer production line, which is efficient in the wrong direction.

Object pools are useful when objects have uniform shape and repeated lifetimes, but I use them cautiously. A pool retains memory, complicates construction and may conceal an unbounded queue. Per-thread caches can reduce contention while increasing total memory consumption. Peak resident size matters as much as allocations per second on a shared machine.

Measurement must include realistic concurrency and duration. A short benchmark can reward an allocator for keeping every page. A long-running service eventually reveals fragmentation and cache growth. I watch CPU profiles, allocation counts and resident memory rather than treating one throughput number as a complete biography.

I prefer changing ownership and allocation frequency before replacing the general allocator. When patterns are genuinely general and contention remains measurable, testing another mature allocator is reasonable. In this case, the region was the stronger result because it made the program’s lifetime visible. I had gone looking for a faster malloc() and found that asking for less was cheaper.

Zero Copy Is Mostly Fewer Copies

I wrote a file server in the most direct way possible: read a block into a user-space buffer, then write that block to the socket. The code was simple and, for small files, entirely satisfactory. With large transfers, the process spent an impressive amount of CPU moving bytes it never inspected.

The ordinary path copies file data from the kernel’s page cache into my buffer and then back into kernel-managed socket buffers. It also crosses the system-call boundary repeatedly. The data needs to travel from disk or cache to the network device, but my temporary ownership adds work without adding information.

Linux offers sendfile() for this case. It transfers data between a file descriptor and a socket while keeping the payload out of the application’s address space. The kernel can connect the file and networking paths more directly, reducing copies and context transitions. “Zero copy” is convenient shorthand, not a sworn statement that no hardware or kernel component ever copies a byte.

The loop still has to handle partial progress. sendfile() can transfer fewer bytes than requested, be interrupted, or return EAGAIN for a nonblocking socket. I keep an offset and remaining length in the connection state, request another writable notification when necessary, and continue later. Replacing read() and write() with one call does not replace flow control.

Headers complicate matters slightly. A protocol header may be generated in memory while the body comes from a file. I can send the small header normally and use sendfile() for the payload. Trying to eliminate the final tiny copy at all costs generally produces more cleverness than performance.

splice() is a more general Linux mechanism for moving data between descriptors through a pipe, and can connect paths that sendfile() does not cover. It also adds another set of constraints and bookkeeping. For serving regular files, sendfile() expresses the intention clearly, so that is where I start.

There are cases where a user-space buffer belongs in the path. Compression, encryption in user space, content transformation and checksums may require examining the bytes. Small responses may show no measurable benefit. Portability also matters; this is an operating-system facility, not ISO C acquiring telepathy.

I strongly prefer the kernel-assisted path when the application is merely forwarding an unchanged large file. The preference comes after profiling, not before. In my test it lowered CPU use while maintaining throughput, which leaves the processor available for actual work. The original loop remains easier to explain, but explaining why a server copies every byte twice is not especially satisfying.