| Episodes | 82 |
| Mentions | 129 |
| Cited here | 36 |
| First — last | #35 — #723 |
| Top guests | Elecia White, Vincent Himpe, Mike Engelhardt |
| Related | microcontroller · compiler · embedded system · raspberry pi · low power design |
Assembly language is a low-level programming language in which source instructions correspond closely to the machine instructions of a specific processor architecture, and which an assembler converts into executable machine code.[187] It occupies the floor of practical programming: the machine code beneath it is an academic exercise, while assemblers themselves offer higher-level constructs such as macros and loop directives that substitute the underlying jump instructions.[468] Assembly matters because it is the layer at which compiled programs ultimately execute, because it grants exact control over instruction selection and timing, and because reading it remains the definitive way to understand what a processor is actually doing.[187][450]
Relationship to compiled languages
A compiled language reaches machine code through assembly: the compiler emits assembler, which the assembler then converts into machine code.[187] Assembly is therefore present in every compiled program whether or not the programmer ever writes it, and the relationship between high-level source and the generated instructions is a practical concern rather than a theoretical one. Inefficient generated code can remain invisible until a device is nearly full, at which point the connection between source and resulting instruction count becomes the thing that has to be understood.[541]
Writing in assembly does not by itself produce efficient code: a good compiler will beat a poor assembly programmer, so the language choice guarantees nothing about the result.[267] The belief that assembly is automatically an order of magnitude faster or lower power is mistaken; it is not a magic bullet, and a compiler carries most designs most of the way.[267] Contemporary compilers are good enough that the genuine remaining cases for hand-written assembly are narrow — single-instruction multiple-data (SIMD) operations, for instance, which high-level source provides no reliable way to trigger.[356] Startup and bootstrapping code, traditionally an assembly preserve, can on modern processor families be written in a high-level language instead.[356]
A productive working method treats assembly knowledge as a diagnostic instrument rather than an authoring tool: the engineer disassembles the compiler’s output, judges it, and then rewrites the high-level source so that the compiler produces the intended instructions, rather than replacing the compiler’s work by hand.[275] Where a vendor-supplied library exists for a signal-processing function, the sensible practice is to call it and let the compiler map it onto the processor’s specialised instructions, rather than writing the routine at instruction level.[213] Code generation from modelling environments exists for the same reason: nobody wants to hand-translate a validated simulation into assembly, and converting the model to a compiled language without introducing errors is what shortens time to market.[39]
Reading generated assembly
The recommended way to acquire assembly is not to study it directly but to read what the compiler produces from one’s own high-level code.[187] Opening the generated assembly alongside the source is the practical remedy when an engineer believes the code is correct but performance is not improving, because a single line of high-level source can expand into a great many instructions.[187] What that inspection reveals is the true cost of operations that look cheap in source form: a floating-point division pulls in a library routine costing hundreds of instructions, which is frequently the explanation for an interrupt handler that takes far too long.[187] Division in general, and floating-point division inside an interrupt in particular, is a recognised error, as is any form of waiting inside an interrupt.[187]
An assembly background is also what makes reverse engineering approachable, since reading disassembled firmware is the core activity and the tools assume that fluency.[450]
Working at the instruction level
Writing assembly means working from the register model published in the device’s documentation: which registers exist, what they are used for, which instructions act on which of them, and how many clock cycles each takes.[444] The programmer at that level need not reason about the micro-instructions beneath each operation, though the cycle counts in the documentation indirectly expose them along with any pipelining the implementation performs.[444] Instructions differ in the number of clock cycles they consume, which is what makes cycle-accurate emulation of a processor both possible and necessary.[599]
Programming in a compiled language still involves thinking in terms of registers and register transfers, so an understanding of what happens in the hardware carries over even when no assembly is written.[444]
Where hand-written assembly is warranted
Hand-written assembly earns its place at the asymptote, where the last increments of performance are being extracted from a part that cannot be changed.[267] Assembly remains the tool where every cycle is accounted for: a fixed-point mathematics library for motor control required a large quantity of assembly precisely because it had to be optimised.[218] An audio signal-processing design was written first in a high-level language and later converted to assembly when every remaining cycle was needed, alongside a move to fixed-point arithmetic in the absence of hardware floating point.[513] Assembly is likewise used where a system must make very fast decisions from sensor input rather than perform complicated computation, with the whole of a custom controller coded that way to get the most from the part.[568]
Generating a video signal directly from a microcontroller depends on assembly loops timed by hand, since the output waveform is produced by instruction timing rather than by a peripheral.[469] Widely used libraries can depend on hand-optimised instruction timing to the point that a simulator must be cycle accurate to run them: driving addressable LED strings relies on assembly whose timing must be exact or the devices do not respond.[599]
A vendor’s documented workaround for a silicon defect can require a register to be written from assembly, which is a legitimate reason for a single assembly routine to exist in an otherwise high-level project.[442] Self-modifying code is the most efficient technique available where every cycle counts and simultaneously among the best ways to make a system unmaintainable.[334]
Economics
Writing an entire product in assembly was economically rational where the alternative was a larger part: spending tens of thousands of dollars of engineering to save a few cents on each of a million units was the standard trade.[465] The calculation runs by volume. Fitting an application into the smallest available part is a false economy at low volume — a hand-coded program occupying all but a byte or two of a one-kilobyte device leaves no room to add anything later — while at volume the few cents saved by choosing the smaller part outweighs the engineering time spent squeezing into it.[187]
Waiting for cheaper and faster silicon is frequently a better use of time than optimising, given how quickly processor cost and performance move.[187] Throwing processing capability at a problem rather than optimising it is sound engineering when it is the practical method, and does not need to be the most difficult or impressive approach available; hand-coding a vision-capable robot in assembly to fit a four-kilobyte part, purely as a challenge, is doing it wrong.[267] Deliberately constraining a design to an eight-bit part has also been defended on the opposite ground: the limits force the design to cover only what is needed and push the engineer toward more inventive solutions, with assembly reserved for the places extra performance is required.[60]
Vendor-supplied library code can be startlingly expensive in space terms — a serial port routine costing ten kilobytes — which is what motivates working closer to the hardware.[490] Interpreted firmware on a small part carries a measurable but modest overhead, with one implementation reserving half of a thirty-two-kilobyte memory as heap and using only a few kilobytes of it; even a high-performance interpreted environment cannot match assembly or a compiled language on speed, since the abstraction has a cost that has to be paid somewhere.[323] An interpreted language on a microcontroller can nonetheless accommodate assembly directly: one implementation lets a function be marked with a decorator so its body is written in processor assembly and compiled to a native routine when the firmware is built.[323]
If processor performance stops improving, the economics change again: spending years on optimising compilers and hand-coded assembly routines starts to make sense because the performance cannot be obtained by waiting.[84]
Assembly-like programming beyond the core
Programmable input-output blocks are programmed in an assembly-like language and let simple pin manipulation be offloaded from the processor core, so combining or conditioning signals costs no processor cycles at all.[528] The same idea appears as small dedicated processors alongside the main core, programmed in assembly for a custom interface block and doing nothing but processing input and output.[595]
Assembly can also be generated at run time rather than written: one circuit simulator authors an assembly program tailored to the specific circuit being solved, assembles it, links it and calls the result as a function, gaining a factor of three in speed.[196] The gain comes from eliminating indirection: with dynamic memory allocation a reference in a high-level language resolves through the address of an address, and for a workload dominated by repeatedly assembling and solving matrices it takes longer to move operands into the arithmetic unit than to perform the arithmetic.[196]
History
Early microprocessor work had no operating system beneath it, so everything was assembly and the machine was bootstrapped by hand.[603] With a one-kilobyte part as the standard option, a compiled language consumed most of the available space in overhead alone, so assembly was the only way to fit — a constraint that was once unavoidable rather than chosen.[267] Custom processors of that era also obliged their users to build the tool chains, since no established compiler existed for a bespoke parallel architecture.[465] The absence of an affordable compiler has itself forced the choice, with one project written entirely in assembly because the vendor’s compiler was unaffordable and no open-source alternative was known.[375]
Applications of the era reached substantial scale in assembly: one integrated product amounted to roughly a quarter of a million lines written by three people in nine months.[603] Writing a major application entirely in the assembly of one processor was also a deliberate commercial bet — a spreadsheet written from the ground up in assembly for a single hardware platform, whose success followed from that platform winning — and the gamble of tying a product to a single machine in exchange for the performance the language afforded was recognised at the time.[122][241]
Every layer of abstraction has attracted the same objection, with assembly programmers having argued that the compiled language destroyed the discipline by hiding the architecture.[39]
Longevity and tool chains
Assembly persists in legacy products where there is no choice about the language, independently of whether it would be chosen for new work.[267] An older architecture can stay in use partly because its patents have expired and no licence fee is owed, and partly because a body of people still write assembly for it and the tooling burden is lighter than for a newer part.[326]
Assembly source is unusually durable: code written for a microcontroller some twenty-five years earlier was pasted into a current version of the vendor’s tools, assembled without modification and programmed into a part successfully.[524] That durability depends on the part remaining popular rather than on the language, so the real compatibility risk lies in designing with a device that falls out of use.[524] Modern tools reassembling old source will warn about practices such as manual bank switching that were necessary when fitting a program into a kilobyte of program memory.[565] The corresponding practical risk is the tool chain rather than the code, which is why an engineer supporting a client project will install the vendor’s development environment into a virtual machine and freeze it.[524]
Learning and practice
Starting in whatever language a newcomer already knows is defensible because the endpoint of optimisation is the compiled language or assembly regardless, and the intermediate learning happens along the way.[235] Teaching bare-metal assembly on a single widely owned board removes platform variation from the exercise, which is what makes such courses practical to follow.[235] Simulators that step through assembly while displaying the processor registers, address and data buses, memory and program counter make the relationship between instructions and hardware directly observable.[528]
Some practitioners work in assembly by preference. Proximity to the hardware is given as one reason — the freedom to generate video and audio signals in real time and the sense that the result is entirely one’s own rather than something a tool produced.[247] An engineer working on time-critical signal processing may reject a compiler on the grounds that they cannot know what it will generate and need direct control of the processor.[169] Assembly optimisation has also been characterised as far more entertaining than it is useful.[256]