Programming

From Qt Slots to Small Tools

Most of my systems work still begins in C. If the job needs a user interface, it usually grows a Qt shell. If it needs to be finished before lunch, a Python script appears beside it and quietly becomes permanent.

That arrangement works, but every boundary charges rent. The C tool has ownership rules, the Qt side has objects and signals, and the Python helper has a different deployment story. I have spent an unreasonable amount of time explaining to machines where their libraries live.

I have been trying the experimental Go compiler from the weekly snapshots. It is plainly unfinished, and the language may change under this post, but it occupies an interesting spot: compiled programs, garbage collection, a small syntax, and concurrency that does not begin with a page of pthread ceremony.

My first useful experiment was the sort of filter I normally write in Python:

package main

import (
    "bufio"
    "fmt"
    "os"
)

func main() {
    in := bufio.NewReader(os.Stdin)
    for {
        line, err := in.ReadString('\n')
        if len(line) != 0 {
            fmt.Printf("%d %s", len(line), line)
        }
        if err == os.EOF {
            break
        }
        if err != nil {
            fmt.Fprintf(os.Stderr, "read: %s\n", err.String())
            os.Exit(1)
        }
    }
}

ReadString may return bytes together with os.EOF, so the line is handled before the error. That keeps a final line without a newline. EOF ends input normally; another os.Error gets reported instead of masquerading as a clean finish.

On my snapshot, I build that directly with 6g and 6l. A later snapshot may spell an import or an I/O call differently; that is currently part of the admission price.

The pleasant surprise is not that this is shorter than C. Python already wins that contest while barely getting out of bed. The surprise is that the result is a small native executable and the source still exposes the mechanics I care about. There is no class hierarchy between the input and me.

I do miss deterministic destruction from C++, especially around Qt resources. Garbage collection is not a substitute for closing a file. Go’s defer looks like the intended answer for many such cases, and I plan to abuse it scientifically.

I am not replacing working C or Qt code. That would be migration as a hobby, a particularly expensive hobby. I am moving the little glue programs first and watching where the new language becomes awkward. So far, it feels less like Python made fast and more like C with several recurring arguments removed.

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.