Zig – SIMD: an explanation

Single Instruction Multiple Data: do the same work on several values at once

SIMD stands for “single instruction, multiple data”. A CPU with SIMD support can perform the same operation on several numbers in parallel.

Suppose we want to add these four pairs:

a = [1, 2, 3, 4]
b = [5, 6, 7, 8]

Scalar code conceptually performs four additions:

1 + 5
2 + 6
3 + 7
4 + 8

With SIMD, those values are grouped into vectors, and the program expresses the operation as one vector addition:

[1, 2, 3, 4] + [5, 6, 7, 8]

The result is:

[6, 8, 10, 12]

Zig represents such values with @Vector:

const Vec4i = @Vector(4, i32);

const a: Vec4i = .{ 1, 2, 3, 4 };
const b: Vec4i = .{ 5, 6, 7, 8 };

const result = a + b;
// result is .{ 6, 8, 10, 12 }

The type:

@Vector(4, i32)

means “a vector containing four i32 lanes”. A lane is simply one element of the vector.

Zig permits vectors of booleans, integers, floats, and pointers. Their lengths must be known at compile time. Vector operations use SIMD instructions when the target supports them; otherwise, Zig can lower them to multiple instructions or process the elements individually. In other words, @Vector expresses parallel data operations without tying the source code directly to x86 SSE/AVX, ARM NEON, or another particular instruction set.

Operations are element-wise

Most ordinary operators work lane by lane:

const Vec4f = @Vector(4, f32);

const a: Vec4f = .{ 10.0, 20.0, 30.0, 40.0 };
const b: Vec4f = .{ 2.0, 4.0, 5.0, 8.0 };

const added = a + b; // { 12, 24, 35, 48 }
const multiplied = a * b; // { 20, 80, 150, 320 }
const divided = a / b; // { 5, 5, 6, 5 }
const greater = a > b; // @Vector(4, bool)

This applies to arithmetic, comparisons, bitwise operators, and many mathematical builtins.

One important rule is that Zig does not implicitly mix a vector with a scalar:

const values: @Vector(4, f32) = .{ 1, 2, 3, 4 };
const scale: f32 = 2;

// Invalid:
// const result = values * scale;

The scalar must first be copied into every lane using @splat:

const scale_vector: @Vector(4, f32) = @splat(scale);
const result = values * scale_vector;

// result is .{ 2, 4, 6, 8 }

@splat is the SIMD equivalent of turning 2 into [2, 2, 2, 2].

Turning a vector back into one value

Sometimes a vector calculation ends with a single result. For that, Zig provides @reduce:

const values: @Vector(4, i32) = .{ 1, 2, 3, 4 };

const sum = @reduce(.Add, values); // 10
const minimum = @reduce(.Min, values); // 1
const maximum = @reduce(.Max, values); // 4

A dot product combines element-wise multiplication with a reduction:

fn dot(a: @Vector(4, f32), b: @Vector(4, f32)) f32 {
    return @reduce(.Add, a * b);
}

test "vector dot product" {
    const std = @import("std");

    const a: @Vector(4, f32) = .{ 1, 2, 3, 4 };
    const b: @Vector(4, f32) = .{ 5, 6, 7, 8 };

    try std.testing.expectEqual(@as(f32, 70), dot(a, b));
}

The calculation is:

(1 × 5) + (2 × 6) + (3 × 7) + (4 × 8) = 70

Loading vectors from arrays and slices

Fixed-length arrays and vectors can be converted directly when their element types and lengths match:

const array: [4]f32 = .{ 1, 2, 3, 4 };
const vector: @Vector(4, f32) = array;
const another_array: [4]f32 = vector;

A fixed-length portion of a slice can also be loaded through a pointer-to-array:

fn loadFour(values: []const f32) @Vector(4, f32) {
    return values[0..4].*;
}

This requires the slice to contain at least four elements.

A realistic function usually processes full vector-sized chunks, followed by a scalar tail:

fn scale(output: []f32, input: []const f32, factor: f32) void {
    const lanes = 4;
    const Vec = @Vector(lanes, f32);
    const factor_vector: Vec = @splat(factor);

    var i: usize = 0;

    while (i + lanes <= input.len) : (i += lanes) {
        const values: Vec = input[i..][0..lanes].*;
        const scaled: Vec = values * factor_vector;
        output[i..][0..lanes].* = scaled;
    }

    // Elements that do not fill an entire vector.
    while (i < input.len) : (i += 1) {
        output[i] = input[i] * factor;
    }
}

For an input length of 11, the vector loop handles elements 0..3 and 4..7; the scalar loop handles elements 8..10.

Masks and selection

A vector comparison produces a boolean vector—a mask:

const values: @Vector(4, i32) = .{ -3, 7, -1, 9 };
const zero: @Vector(4, i32) = @splat(0);

const negative = values < zero;
// { true, false, true, false }

@select chooses each result lane according to such a mask:

const clamped = @select(i32, negative, zero, values);
// { 0, 7, 0, 9 }

This implements max(value, 0) for every lane without writing a branch for each element.

Zig also provides @shuffle for reordering, duplicating, or combining lanes. It is useful for operations such as rearranging color channels or preparing values for horizontal calculations.

Vectors are not merely arrays

Arrays and vectors look similar, but communicate different intent:

– [4]f32 means four values stored as an ordinary fixed-length collection.
– @Vector(4, f32) means four values intended for element-wise parallel operations.

They can be converted because both have a defined bit layout. However, vectors do not have a defined byte layout, so pointer-casting between array and vector pointers is illegal behavior. Use value conversion or coercion instead. Zig 0.16.0: Relationship with Arrays

Does @Vector guarantee one SIMD instruction?

No. It guarantees vector semantics, not a particular machine instruction. The compiler may:

– use one native SIMD instruction;
– split a large vector into several native instructions;
– lower an unsupported operation to scalar instructions;
– evaluate the expression at compile time when all inputs are known.

A four-lane f32 vector fits naturally into a 128-bit SIMD register, but the final code still depends on the target architecture, selected CPU features, operation, and optimization mode.

For performance work, compile an optimized build and inspect or benchmark the generated code. Explicit vectors make the intended parallelism clear, but they do not remove the need to
measure.

Once again: software engineering is about managing complexity

There is a misunderstanding about software engineering that AI is making increasingly visible: we tend to confuse writing code with building software.

There is some overlap in certain aspects, but they’re not the same thing.

Writing code means translating an idea into instructions a computer can execute. Building software means deciding which instructions should exist in the first place, how they should interact, which constraints matter, what’s the cost of the decisions, which tradeoffs can be considered acceptable, and how the resulting system can evolve without collapsing under its own constraints and limitations.

Let’s start from a basic premise: AI is an essential tool because it is remarkably good at the first problem.

The second is where software engineering actually begins.

The difficult part was never typing the code

Consider a relatively ordinary engineering requirement.

We need to process incoming events and update some data.

And these are some of the first questions that arise during a technical discussion:

  • Should we process them synchronously?
  • Should we put them into a queue?
  • Do we need exactly-once processing, or is at-least-once sufficient?
  • Can the system tolerate eventual consistency?
  • What happens when processing fails halfway through?
  • Should we retry? How many times?
  • What happens if the consumer is unavailable for three hours?
  • Do events need to preserve ordering?
  • How much traffic do we expect today? What about two years from now?
  • What are the consequences if an event is processed twice?

… and so on.

These questions have very little to do with syntax.

Programming language’s choice matters, because affects team’s fluency, team’s performance, system’s performance, safety, maintainability, tooling, and operational characteristics, but it does not answer the fundamental questions.

The difficult part is choosing the architecture that represents the right set of compromises.

And there is rarely a universally correct answer.

1 problem can have N completely different correct solutions

This is particularly obvious when software exists inside a business.

Imagine two companies asking their engineering teams to build what sounds like exactly the same feature.

Their requirements may look identical on paper.

But:

  • Company A has 500 users while Company B has 20 million.
  • Company A may have three engineers maintaining the system. Company B may have 200.
  • One may require strong consistency because mistakes have serious financial consequences. The other may happily accept eventual consistency in exchange for availability and throughput.
  • One company may need to ship in three weeks.
  • Another may expect the system to remain operational for fifteen years.
  • One may already have Kafka, Kubernetes, PostgreSQL, observability infrastructure, and engineers experienced with distributed systems. Another may have a single application server and a PostgreSQL database maintained by four developers.

The technically impressive solution for one company could be an irresponsible solution for the other.

This is why architecture cannot be reduced to asking:

What is the best way to implement X?

The correct question is usually closer to:

Given these constraints, this team, this business, this infrastructure, this budget, these risks, and the expected evolution of the product, what is the most appropriate way to implement X, today?

That is a radically different question.

AI generates solutions. Engineers own tradeoffs.

This distinction is becoming increasingly important because of how AI is being adopted by software organizations.

AI is extraordinarily useful for software development. We use it as an accelerator: generating boilerplate, exploring APIs, proposing implementations, finding potential bugs, explaining unfamiliar code, producing tests, comparing approaches, or simply reducing the amount of mechanical work required to turn an idea into working code.

But there is a dangerous tendency to extend this capability into something much broader:

delegating engineering judgment itself

You can:

  • give an AI model a requirement and ask it to design the system, “no mistakes”: It will design one.
  • ask it to choose a database: It will choose one.
  • ask whether you should introduce a queue, microservices, caching, CQRS, event sourcing, Kubernetes, Redis, or another abstraction: It will give you an answer.

The existence of an answer, however, does not mean the underlying engineering problem has been solved.

The real problem is that the correct decision depends on context, often an enormous amount of context, that needs hours, days, or even weeks to analyze, understand, and evaluate, often involving multiple departments within the same organization, and frequently leaving gray areas that are difficult to formalize and may become a challenge when future changes are required.

This is how real world works: some of that context exists in documentation. Much of it does not.

It exists in conversations with customers. In the history of the product. In the skills of the engineering team. In operational incidents from three years ago. In budget constraints. In deadlines. In contractual obligations. In the weird behavior of a legacy system nobody wants to touch. In knowing that a customer is likely to request a particular feature six months from now.

And sometimes it exists simply in experience: recognizing that a theoretically elegant architecture will become an operational nightmare for the team expected to maintain it.

You cannot treat all of this as a small detail that will somehow be captured by adding another paragraph to an AI prompt.

There is no architecture without tradeoffs

Engineering is largely the discipline of deciding which problems you are willing to have. Even if you don’t work at FAANG scale, your projects may still need to process enough data that making the right engineering decisions becomes essential to maintaining adequate performance, without having to throw expensive hardware at systemic inefficiencies.

Even “simple” choices come with factors that need to be considered:

  • Normalize the database aggressively, and you may gain integrity while making certain queries more complicated.
  • Denormalize it, and you may improve read performance while increasing synchronization complexity.
  • Add caching, and you reduce latency while introducing invalidation problems.
  • Introduce asynchronous processing and you decouple components while creating new failure modes.
  • Split a monolith into microservices and you gain independent deployment boundaries while introducing distributed-system complexity.
  • Optimize aggressively and you may gain performance while sacrificing readability.
  • Abstract aggressively and you may reduce duplication while making the system harder to understand.
  • Avoid abstraction and the opposite problem eventually appears.

None of these can be evaluated independently. Every optimization spends complexity somewhere. The job of an engineer is therefore not simply to make the system work.

It is to decide where complexity belongs.

Algorithmic thinking matters more, not less

This is why I think algorithmic thinking becomes more important in the age of AI, not less.

And by algorithmic thinking I do not simply mean knowing textbook algorithms or being able to implement a red-black tree from memory.

By algorithmic thinking, I mean defining a set of basic rules to follow and applying everyday. I’ve put together a ten-point set of principles:

  1. Decompose the problem.
  2. Identify invariants.
  3. Understand data flows.
  4. Choose appropriate data structures.
  5. Reason about time and space complexity.
  6. Identify contention.
  7. Understand failure modes.
  8. Recognize which operations are expensive and which ones actually matter.
  9. Separate essential complexity from accidental complexity.
  10. Understand what should happen when assumptions stop being true.

I know, in the time of boosted productivity, this can sounds weird. AI can generate ten implementations in the time it previously took an engineer to write one. The challenge is to use AI as a tool to apply these principles through targeted, controlled iterations, while keeping a human in the loop.

That makes choosing the right implementation ten times more important. This is what boosted productivity really means.

Your programming language is a tool, not an identity

The same applies to programming languages.

There is value in choosing the right tool for the job. Languages have different strengths, ecosystems, performance characteristics, type systems, concurrency models, deployment models, and safety guarantees.

Those differences matter, but engineering discussions sometimes give programming-language choice “overvaluation”. In many business systems, choosing between two reasonable languages will matter considerably less than choosing the wrong architecture, the wrong data model, or the wrong operational strategy.

Pick a language appropriate for the scenario. If several languages are appropriate, there is significant value in choosing the one your team understands deeply. Don’t base your decisions on the assumption that LLMs perform better with one PL or another. Always put the team first.

The 5-matters rules are:

  • Fluency matters.
  • Ecosystem maturity matters.
  • Maintainability matters.
  • Debugging matters.
  • Deployment matters.

This is why, today more than ever, with LLMs at our disposal, I encourage engineers not to be religiously loyal to a single PL, but to become as polyglot as possible, learning to use AI assistants as learning tools, not merely as code generators, always double checking official docs.

The real optimization target is the lifetime of the system

Another mistake is optimizing software exclusively for its current requirements.

Modern software cannot be static, and it must be built to respond to following changes:

  • Users
  • Customers
  • Traffic
  • Business models
  • Regulations
  • Teams
  • Infrastructures

Every point has a weight that can change overtime. So the feature that appears insignificant today can become the core workflow two years later.

Good engineering therefore requires some ability to anticipate change without falling into the opposite trap of designing elaborate abstractions for hypothetical futures that may never arrive, just because teams accepted some codex output, too long to be fully reviewed. That balance is difficult, because:

  • How likely is this requirement to change?
  • Where should we deliberately introduce flexibility?
  • Where should we keep things simple?
  • Which technical debt is acceptable?
  • Which technical debt could become structurally expensive?
  • What should we optimize now?
  • What should we deliberately leave unoptimized until measurements prove that it matters?

This is engineering judgment.

There is no AI that can correctly answer these and many other questions, nor can you simply put an AI agent to work on them and expect a reliable outcome. There is no prompt that makes those questions disappear.

AI-generated code creates a new ownership problem

There is another issue that I think software organizations are underestimating: codebase ownership.

AI makes producing code extraordinarily cheap. That sounds entirely positive until you remember that producing code and maintaining code are two different economic activities. An engineer can now generate hundreds or thousands of lines of apparently reasonable code extremely quickly.

A team can merge them, tests pass, the feature works, productivity appears to have increased, but something critical may have been lost. And if you want to understand what, ask yourself:

  1. Does the team actually understand the general architecture?
  2. Do they understand the data structures?
  3. Do they understand why a particular abstraction exists?
  4. Do they understand the failure behavior?
  5. Do they know which assumptions the generated implementation depends on?
  6. We can assume they could modify it confidently six months later without asking another AI to explain the code that AI previously generated?

If the answer is no, the team has not eliminated complexity. It has probably introduced others, which become increasingly relevant over time.

Lines of code are cheap. Understanding is expensive.

This is perhaps the biggest change AI introduces into software economics.

Historically, producing code was relatively expensive. Now it is becoming extremely cheap. While understanding a system, it’s becoming increasingly expensive

That distinction matters: a pull request containing 3K lines of AI-generated code is still a 3K-line addition to the cognitive surface area of your system. And no, a passing-test suite is not the answer, because it gives you a false sense of confidence that everything’s under control because the system -apparently- works as intended.

If you ship it, you own it

For this reason, I believe strong technical management should maintain a very simple principle:

Never ship code you do not own.

What do I mean by “ownership”? Having a complete understanding of the codebase, having validate every part of it.

AI-generated code should not receive a lower standard of scrutiny because it was generated quickly with some advanced/expensive/frontier/whatever LLM model.

The challenge now is to determine whether (and when) it makes sense to invest time in reviewing such large pull requests, or whether it is better to move more slowly through smaller iterations, where small chunks of application logic are developed through a controlled human–AI exchange.

Software engineering may actually be getting harder

As a final point, I want to focus on an interesting paradox that seems to have emerged recently:

AI makes programming easier while potentially making software engineering harder.

We can produce more code, faster than ever before. We can create prototypes almost instantly. We can integrate unfamiliar libraries without spending days reading documentation. We can generate tests, APIs, database layers, infrastructure configuration, frontend components, migrations, and entire services at extraordinary speed.

But our capacity to understand systems has not increased at the same rate. And this creates a new bottleneck, which is no longer:

How quickly can we write this?

But:

How much complexity can this organization actually understand and control?

Productivity metrics we used so far, are broken: a team producing five times more code is not necessarily five times more productive. We haven’t been using AI systems in software engineering long enough to have reliable data yet, but it is becoming increasingly clear that the initial productivity gains we saw during the early stages of LLMs adoption, are now coming at a cost in terms of maintaining and evolving moderately complex systems.

From now on, productivity should therefore not be measured by how much software we generate.

It should be measured by how effectively we solve problems while keeping the resulting complexity under control.

AI should increase engineering leverage, not replace engineering judgment

None of this is an argument against AI-assisted development. Quite the opposite. AI is probably the most powerful productivity tool software engineers have received in decades.

But we should use that leverage to spend more time on engineering, not less. We’re allowed to let AI write the repetitive adapter, generate the first version of the test suite, explain an unfamiliar library. It’s extremely useful to let it help investigate an error, propose 3 implementations or refactor mechanical code. It’s a time saver, a booster, an assistant, combined.

Code was never the product

Software engineering is not merely an academic discipline. We are here to serve our users, so it is a boots-on-the-ground practice, and it needs to remain connected to a real world that is changing faster than ever. It is up to your team to decide how, where, and when AI-generated code fits into your development process.

Programming languages will evolve. Frameworks will disappear. AI models will become dramatically more capable. The cost of generating code will continue approaching zero.

But complexity will remain. It belongs to engineers who can understand the most complexity while introducing the least unnecessary complexity of their own.

AI can help us get there. But it cannot relieve us of our responsibility for what we ship.

LLM’s code is just untrusted text. Until you validate it.

People often ask me what the best programming language is to use with LLMs. One of the strongest options, in my opinion, is Rust. However, before choosing it, you first need to ask yourself whether it’s worth picking a language with manual memory management instead of one with a garbage collector.
And regardless of that decision, the first thing is to understand that LLMs are not deterministic systems.
They should be used for what they actually are: smart suggesters.
You just don’t have to trust suggesters by default.
They read input text.
They responds with output text.
Disclaimer: it’s not code. Not yet.
Even if it looks like code, it compiles as if it was actual code, trust me.
It’s not.
The rule is:
code is in untrusted state, until you validate it.
untrusted state
No matter of the language.
Rust is not different.
Its compiler strictness, doesn’t allow you to break the rule.
But let’s take a step back, in order to understand why have to evaluate other languages, not only Rust.

Manual memory management forces you to obsess over ownership & lifetimes at every step.

The bugs:

  • use-after-free
  • double-free
  • leaks
  • dangling pointers
  • overflows

… are brutal to debug, especially in large or concurrent codebases. Code tends to be complex, and sometimes application logic is more related to the way memory is allocated, not only at the way data is processed. It means there are tons of elements you have to consider in the I/O design, which tends to be exponentially complicated in complex infrastructures, where you can have async processes, threads, locks, concurrent accesses.

In this scenario, people often choose GC languages, because they remove an entire class of problems. You allocate and move on. Cleaner code, simpler APIs, safer concurrency.

But GC has costs. Latency and memory.

For systems, games, kernels, databases, manual control still wins.

On the other side, modern GCs are so highly optimized that, for most software, you can choose them unless your main target is the extreme performance, over productivity and simplicity.

And Rust is a modern language that tries to thread the needle with the borrow checker. It gives you extreme performances at the cost of massive complexity.

Rust teams fight the compiler all day long. Consider for instance, a typical Rust+Tokio project: Rust’s strictness + Tokio’s zero-cost async creates a combination that is extremely safe & fast when done right. But mentally expensive, even with apparently small details like lifetime management. 

This is exactly why some people prefer simpler GC languages.

But there’s a lot of people that choose Rust because of its pattern-heavy nature, and as a consequence, for its reputation of being LLM-friendly.

What they often miss, however, is that this same strictness brings its own inner complexities: the ownership model, borrow checker, and lifetime management can add significant cognitive overhead and development friction that goes beyond what most teams anticipate. If you only consider how easy it is to generate Rust code, but you don’t evaluate properly how much time you’ve to spend to review and validate it, you’re introducing a new generation of technical debt.

This is one of the most underestimated issues: LLMs are excellent at generating code, but they don’t truly understand simplicity or the deeper abstractions in software. They’re just very capable text generators, helping you to get sophisticated scaffolding without any inherent logic, intuition, or understanding of consequences. 

It’s entirely up to you to steer the process: to keep things simple, eliminate unnecessary code, and avoid hidden side effects. And in Rust, believe me, it takes time and effort.

What worries me is how many developers underestimate this aspect, treating this generated “text” as if it were the reliable output of a deterministic system, so there is no need to double check it.
They speak of LLMs as just another clean layer in the stack, comfortably sitting between the human and the machine.
It’s not.
It’s probabilistic text, the result of statistical patterns learned from a vast, uncontrolled corpus of human writing.
This shift has quietly made humans the bottleneck. We no longer need to type code to create software. Or at least, typing is not the only way.
The act of writing it used to force us to think, design, and truly understand what we were building, though. And this cannot be skipped, being delegated to text-generators machines.
They just don’t do it.
Now we write a vague prompt with some requirements and expect the machine to figure out what we actually need. This works surprisingly well for simple, common tasks, which is why it’s fair to call LLMs a powerful autocomplete.
"Given the id parameter, write the SQL statement that updates the user's email"
SQL prompt
Easy. Isn’t it?
Real world development is rarely that linear, though. It often involves complex context that’s extremely hard to describe in natural language, often splitted in various physical and logic spaces where engineering happens.
In those cases, many experienced engineers still find it faster and more precise to write the code directly rather than struggling to find the perfect words to describe what they want.
That’s exactly why formalism exists.

A pure scientist probably prefer to write:

Y=λf. (λx. f (x x)) (λx. f (x x))
Fixed-point combinator
… instead of producing dozens of pages describing fixed-point combinators that turn any function into a recursive version of itself, without needing named functions or loops.
So when they say that:
"In the AI age the problem is no more the code"
AI claim

…they’re omitting some critical details.

Problem is no more related to code itself, as a sequence of letters/words/lines.

“Coding”, in the AI age, is a marketing simplification.

"Coding" !== "Writing software" ∈ "Software engineering"

Writing code, as the act of typing commands, is related to thousands of sw engineering activities. Written code can have ~0 external context, or it can be something that depends and/or affects billions of elements outside of the namespace where it happens.

This is the difference.

And it changes everything.

If you blindly rely on generated text, and you don’t consider or underestimated implications, you’re probably using AI the wrong way.
And no, test coverage is no more enough. Now more than ever.
Test coverage assumptions are (were) a mutual contract between the team and the system. Starting from a bunch of functional requirements, engineers designed the system, and iteratively write tests in order to prove the absence of a predefined set of possible bugs.

Assume they create a system that draws a square, then assert that it has four sides of equal length meeting at 90° angles.
– What if the system creates a rectangle?
– What if it’s a cube instead?
– What if there is an extra-line, in some cases? Tests you defined on the first iterations based on the 1st LLM’s output, cannot consider if/where AI added some extra logic that can cause some unpredictable cases like that.

So every time AI generates some new text, you should evaluate everything, and update test suite accordingly.

This can be convenient in some cases, inefficient in other cases, risky most of the cases, time consuming always.

And you shouldn’t skip this step.

The rule is to:
never ever consider LLM's output as if it was a compiler artifact
text generator

It’s just text that eventually compiles.

It becomes code only after you validate it.

So, to recap, it’s a trust boundary model, for LLM assisted development, where the states are:

…and the flow is:

Raw output from an LLM is always treated as untrusted text (MU-TXT). Only after explicit human review, validation, and acceptance is it promoted to trusted code (MT-TXT → HVC) and allowed to enter the codebase via commit and branch merge.
The loop emphasizes that code generation is an iterative human-driven process, not a LLM/Language/AI based automation.

That’s why the problem is not related to the language you choose, as well as you shouldn’t choose a PL just because it has a good LLM support in that historical period of time. It’s a human issue, not a technical one.

Before validation, you don’t own that codebase. So please choose a language that allows you to be the always owner, not matter how much complex things get in the future.

Conclusions:

It’s a Human In The Loop flow

Whatever the language you choose, the review step has to be considered critical in the development lifecycle. If your management tends to underestimate it and/or tends to trust too much on agents’ related automations, consider it as a red flag.

Ownership is a cognitive responsibility.

It isn’t just “who wrote it”, it’s who holds the mental model. The person who owns the codebase is the one who can answer:

  • Why does this code exist?
  • What tradeoffs were made, and why?
  • Where does it break under pressure?
  • What would need to change if requirements shifted?

A good technical management, never allows shipping a single line of code the team produced, if not fully understood.

 

AdP

Tigerstyle – Software engineering methodology

Do the hard thing today to make tomorrow easy.

1. Safety

  • Correctness is necessary but not sufficient for safety. To be safe, a program must not only run correctly. It must apply defense-in-depth and verify itself while running, to run correctly or else shut down if it detects that it has violated expectations.
  • NASA’s Power Of Ten Rules

    TigerStyle follows the spirit of NASA’s Power of Ten Rules for Safety-Critical Code by Gerard J. Holzmann. For example, static allocation, assertions and explicit limits. Read the Original Rules, which will change the way you code forever.

    “The rules act like the seat-belt in your car: initially they are perhaps a little uncomfortable, but after a while their use becomes second-nature and not using them becomes unimaginable.”

    — Gerard J. Holzmann

  • Explicit Limits

    Put a limit on everything because everything has a limit.Bound all resources, concurrency, and execution. Don’t react to stimuli but use fixed intervals to schedule work. Avoid recursion. Bound loops and queues to detect infinite loops and latency spikes.

    Use fixed types like u32. Avoid architecture-specific types like usize.

  • Static Memory Allocation

    Allocate all memory at startup. Don’t allocate after initialization.

    This centralizes and simplifies resource management, solves fragmentation, forces you to think through “the physics of the system”, and leads to an elegant design, with efficient, predictable performance.

  • Assertions

    Where types check structure, assertions check all logic and state, to detect programmer error, multiply fuzzing, and downgrade catastrophe. Assert arguments, returns, and invariants: what you expect and don’t expect, the positive and negative space, not only contract but breach.

  • Logical Interfaces

    The safety (as well as performance and experience) of a system is dominated by the quality of its interfaces.

    • Minimize surface area
    • Define fault models
    • Abstract physical non-deterministic interfaces with logical deterministic interfaces
    • Push control flow up and data flow down
  • Dimensionality

    Simplify function signatures to minimize branches at the call site, which are viral through the call graph.

    As a return type, bool trumps u64 trumps !u64.

    Minimize or define variables near to when/where they are used, to close semantic gaps in time/space.

  • Zero Dependencies

    Dependencies risk safety and performance, invite supply chain attacks, and increase install times. For infrastructure in particular, these costs multiply up the stack.

    Similarly, tools have costs. A small standard toolbox feels slow for you at first but accelerates the team long term.

  • Zero Technical Debt

    Code, like steel, is easier to change while it’s hot. Do it right the first time, the best you know how, because you may not get another chance, and because quality builds momentum. This is the only way to make steady progress, knowing that the foundations are solid.

2. Performance

  • The lack of back-of-the-envelope sketches is the root of all evil.
  • Think about performance from the outset. The time to solve performance, and get the 1000x wins, is in the design phase, when you can’t profile. It’s hard to fix a system after implementation, and the gains are less. Have mechanical sympathy. Like a carpenter, work with the grain.
  • Primary Colors

    Like a painter, perform back-of-the-envelope performance sketches with respect to the four primary colors (network, storage, memory, compute) and their two textures (bandwidth, latency) to be “roughly right” and land within 90% of the global maxima.

  • Control Plane / Data Plane

    Separate the control plane from the data plane by batching. Use bigger buffers to amortize network, storage, memory and compute costs, and to enable assertions in the control plane without trading performance in the data plane. Let the CPU sprint through large units of work.

  • Make the Roads Bigger

    Think of the future, where things are going. Use bigger block sizes to amortize fixed costs, to minimize control plane overhead, and to place fewer demands on hardware for performance. Embrace concurrency.

  • Zero Copy / Deserialization

    Per core memory bandwidth is a new bottleneck:

    1. Do things in the most direct way possible
    2. Don’t copy memory in the data plane
    3. Don’t thrash the CPU cache
    4. Don’t serialize or deserialize data
    5. Use fixed-size cache line aligned structs
    6. Align structs to their largest field

3. Experience

  • Together, we serve the users.
  • A day of design is worth weeks or months in production. Therefore, go slow to go fast. Optimize the total cost of software ownership, not for those who write it once, but for those who read and run it many times. Trade linear deadlines for exponential quality.
  • Simplicity And Elegance

    “…simple and elegant systems tend to be easier and faster to design and get right, more efficient in execution, and much more reliable, [but] require hard work and discipline to achieve…”

    — Edsger Dijkstra

  • Nouns And Verbs

    Great names are the essence of great code, capturing what a thing is or does, for a crisp mental model.

    1. Append qualifiers to names
    2. Sort by most significant word (big endian naming)
    3. Use the same number of characters for related names (e.g. source/target) so they line up in the source.
    4. Use snake_case
    5. Don’t abbreviate
  • Biodigital Jazz YouTube icon

    Software is engineering, and more, it is art. Information and inspiration. Reason and experience. Precision and poetry. Just like music. A tight beat. A rare groove. Words that rhyme and rhymes that break. If you believe in the songs, take them on tour. Making is showbiz.

    If you want to be remembered, be remarkable.

If you want to be remembered, be remarkable.

 

ref: https://tigerstyle.dev/

 

AdP