4.6.2.1 - High-level and low-level languages
Programming languages differ in how closely they expose the processor's instructions. Compare machine code, assembly and imperative high-level languages, then justify a choice for a particular task.
Machine code and levels of abstraction
Every executable program ultimately depends on instructions that the target processor can carry out, even when the programmer writes in a more convenient notation.
Machine code is the binary representation of instructions in a processor's instruction set. An instruction specifies an operation and, where needed, operands or ways to locate them. A bit pattern only has the intended instruction meaning when interpreted by a processor supporting that instruction set. A different processor family need not interpret it in the same way.
Low-level languages are close to the machine's operations and organisation. Both machine code and assembly language are low-level. High-level languages hide more machine detail and express operations in terms closer to a problem, such as manipulating strings or repeating an action over a collection.
Historically, writing raw machine instructions gave way to symbolic assembly notation, then higher-level languages offered more abstraction and improved productivity. These forms still coexist: newer abstraction does not mean older forms have disappeared. The classification describes distance from hardware, not whether a language was invented recently or whether it is “powerful”.
Writing machine code directly gives exact control over instruction bytes and does not require a translator for those instructions. However, long binary instruction sequences are difficult to read, write and debug; manually encoding an operand or opcode invites mistakes. Programs are closely tied to the target instruction set, making porting to a different architecture difficult.
Low-level language
A language closely tied to a processor's instruction set and hardware operations. Machine code and assembly language are both low-level languages.
Assembly language uses symbolic instructions
Assembly makes individual machine operations more readable without removing their dependence on the target architecture.
Assembly language uses mnemonics for operations and symbolic names or numbers for operands, registers and addresses. A mnemonic such as ADD is easier to recognise than an instruction's encoded binary opcode. Labels can identify locations without the programmer manually calculating every numeric address. An assembler converts this source into machine code for the target processor.
There is generally a close correspondence between a basic assembly instruction and a machine instruction. Assemblers can also provide directives or macros, so claiming that every line always becomes exactly one machine instruction would be too strong. The key point is that assembly exposes low-level operations rather than combining a complex user task into one high-level construct.
Compared with raw machine code, assembly is easier to read and change, but it still requires knowledge of registers, instructions and memory operations. Compared with a high-level language, it usually needs more instructions to express the same substantial task and is harder to develop and maintain. Assembly for one instruction set is not automatically portable to another.
Assembly can be useful when precise hardware access or tight control of instruction sequences is required, for example a small processor-specific routine in an embedded system. It offers the opportunity to tune a critical routine, but hand-written assembly is not guaranteed to be faster than carefully optimised compiled high-level code. The result depends on the programmer, compiler and task.
Imperative high-level languages and language choice
A programmer can describe changes to a solution's state without naming each processor register used to make those changes.
An imperative high-level language expresses a program as instructions that change state: assignments update values, selection chooses actions and iteration repeats actions. Variables, data types, structured control and subroutines let the programmer work above individual machine instructions. The same conceptual operation may require many machine instructions after translation.
High-level source still needs a route to execution, such as compilation or interpretation; being readable does not make it directly executable by arbitrary hardware. A translator/runtime performs lower-level operations while the programmer works with higher-level constructs. The source can often be moved between platforms where a suitable implementation exists, although hardware-specific libraries or services may still limit portability.
| Consideration | Low-level approach | High-level approach |
|---|---|---|
| Development and maintenance | Detailed instruction/memory work; usually more difficult and time-consuming | Abstractions and reusable facilities usually reduce code and effort |
| Hardware control | Direct architecture-specific control | Detail normally handled by translator, runtime or libraries |
| Portability | Closely tied to the instruction set | Source often portable with compatible tools and dependencies |
| Efficiency | Potential precise tuning, but considerable programmer effort | Compiler optimisation can be effective; abstractions/runtime may add overhead |
For a multi-platform booking application, readable maintainable high-level code and portability are likely more valuable than manually choosing instructions. For a very small timing-critical device routine, architecture-specific control might justify assembly and its additional cost. A good justification connects an attribute to a requirement instead of declaring one language level always superior.