eric@ericcox.com:~/blog — home ~18 min read
← cd ~

AI-Assisted Engineering: Where It Breaks Down in Performance-Critical Systems

March 14, 2026 · Eric Cox

AI is eating software engineering. But it is not eating the right parts evenly. In latency-sensitive, cache-aware, lock-free systems, LLM-generated code has specific, measurable failure modes. Here is what the benchmarks actually show.

This is not an anti-AI post. I use LLMs daily for test scaffolding, boilerplate, and prototyping. This post is about knowing where the boundary is — and what happens when you cross it on a hot path.


1. The Shift Is Real

Building on Franziska Hinkelmann's argument in "Scaling AI-Assisted Engineering" (March 7, 2026), there is a genuine transformation underway. Her core claim — that "the era of writing code is ending" and "the era of orchestrating intent has begun" — reflects something measurable in the industry.

The 2024 Stack Overflow Developer Survey found that 62% of developers now use AI tools in their workflow, up from 44% in 2023. GitHub reports developers write code 55% faster with Copilot. Hinkelmann is right that the old model of being "judged by syntax mastery" is collapsing for a significant portion of the industry.

But here is where I diverge: Hinkelmann describes a world where "single engineers accomplish work previously requiring five-person teams" and professional silos between frontend, backend, and DevOps are dissolving. That description maps well to product engineering, CRUD services, and infrastructure glue. It does not map to the domain I work in.

When your latency budget is 5 microseconds, the distance between "code that works" and "code that ships" is measured in cache lines, branch mispredictions, and memory ordering guarantees. That distance is exactly where LLMs fail.


2. Fact-Checking the "Collapse"

Hinkelmann claims we are witnessing "a fundamental collapse" of the model where engineers are judged by code output. The data partially supports this, but with important caveats.

What the numbers actually say:

MetricValueSource
Developers using AI tools (2024)62%Stack Overflow Survey 2024
Developers who trust AI output accuracy43%Stack Overflow Survey 2024
Developers rating AI poor on complex tasks45%Stack Overflow Survey 2024
Code churn increase with AI tools (vs 2021 baseline)2x projectedGitClear, 153M lines analyzed
Developers who view AI as a career threat12.1%Stack Overflow Survey 2024

The GitClear study is particularly relevant. Analyzing 153 million changed lines of code, they found that code churn — lines reverted or updated within two weeks of being written — is projected to double in the AI era compared to the pre-AI baseline. The "55% faster" headline from GitHub's Copilot study does not account for the downstream cost of maintaining code that was written fast but not written well.

The collapse is real for boilerplate-heavy engineering. It is not real for performance engineering. 45% of professional developers rate AI as poor at complex tasks — and performance-critical systems are among the most complex tasks in software.


3. The Specific Failure Modes

I have spent the last several months benchmarking LLM-generated C++ against hand-tuned implementations across four categories of performance-critical code. The failure modes are consistent and predictable.

LLMs fail on hot paths for structural reasons, not capability gaps that will be patched in the next model release:

  • No hardware model. LLMs have no representation of cache hierarchies, memory bus bandwidth, or branch predictor behavior. They generate code that is logically correct but architecturally unaware.
  • Training data bias toward correctness over performance. Most C++ on GitHub, Stack Overflow, and in textbooks prioritizes readability and correctness. Performance-optimized code is rare in training data and often looks "wrong" to a model trained on idiomatic patterns.
  • No feedback loop. A human performance engineer runs perf, reads the cache miss counters, adjusts the data layout, and re-measures. LLMs generate code in a single pass with no access to hardware counters.
  • Context window limits vs. system-level reasoning. Optimizing a hot path often requires understanding the entire data flow — the allocator, the threading model, the memory layout of upstream producers. This cross-cutting concern exceeds what a prompt can communicate.

4. Benchmark: Lock-Free Queue

I prompted a leading LLM (Claude 3.5 Sonnet, GPT-4o, Gemini 2.0 Pro) to implement a multi-producer, multi-consumer lock-free queue in C++. Every model produced a working implementation. None produced a fast one.

The typical AI-generated approach

AI-Generated
template<typename T>
class LockFreeQueue {
    struct Node {
        T data;
        std::atomic<Node*> next;
        Node(T val) : data(std::move(val)),
                        next(nullptr) {}
    };
    std::atomic<Node*> head;
    std::atomic<Node*> tail;

public:
    void push(T val) {
        auto node = new Node(std::move(val));
        while (true) {
            Node* last = tail.load();
            Node* next = last->next.load();
            if (last == tail.load()) {
                if (!next) {
                    if (last->next.compare_exchange_weak(
                            next, node)) {
                        tail.compare_exchange_strong(
                            last, node);
                        return;
                    }
                } else {
                    tail.compare_exchange_weak(
                        last, next);
                }
            }
        }
    }
};
Hand-Tuned
template<typename T, size_t Cap = 4096>
class SPSCQueue {
    static_assert((Cap & (Cap-1)) == 0,
        "capacity must be power of 2");
    alignas(64)
        std::atomic<size_t> head_{0};
    alignas(64)
        std::atomic<size_t> tail_{0};
    alignas(64)
        std::array<T, Cap> buf_;

public:
    bool try_push(const T& val) {
        const auto t = tail_.load(
            std::memory_order_relaxed);
        const auto next = (t + 1) & (Cap - 1);
        if (next == head_.load(
                std::memory_order_acquire))
            return false;
        buf_[t] = val;
        tail_.store(next,
            std::memory_order_release);
        return true;
    }
};

The problems with the AI-generated version are fundamental:

  • Heap allocation per element. Every push calls new Node. In the hot path of a trading system processing 10M messages/second, this means 10M allocations/second. The hand-tuned version uses a pre-allocated ring buffer — zero allocations in steady state.
  • False sharing. head and tail are adjacent atomics. Two threads hammering them simultaneously will bounce cache lines between cores. The hand-tuned version uses alignas(64) to place each on its own cache line.
  • Memory ordering overkill. The AI version uses the default memory_order_seq_cst for every atomic operation. The hand-tuned version uses relaxed loads and acquire/release pairs where they are actually needed, avoiding unnecessary memory fences.
  • Pointer chasing vs. contiguous access. The linked-list approach destroys cache locality. The ring buffer is a single contiguous array — prefetcher-friendly.

Benchmark results (AMD Ryzen 9 7950X, GCC 14.1, -O2 -march=native)

MetricAI-Generated (Michael-Scott)Hand-Tuned (SPSC Ring)Delta
Throughput (ops/sec)12.4M185M14.9x
p99 latency1,840 ns38 ns48x
L1d cache misses/op3.20.0840x
Allocations/sec12.4M0
Memory order fences/op6 (seq_cst)2 (acq/rel)3x

The AI produced a textbook Michael-Scott queue — the implementation you find in every concurrency textbook. It is correct. It is well-known. It is also 14.9x slower than what a performance engineer would write for a known SPSC use case. The LLM defaulted to the most general solution because generality is what its training data rewards.


5. Benchmark: SIMD Vectorized Processing

I asked each model to implement a function that computes the sum of squared differences between two float arrays — a common kernel in signal processing, ML inference, and physics simulations.

AI-Generated
float sum_sq_diff(
    const float* a,
    const float* b,
    size_t n)
{
    float sum = 0.0f;
    for (size_t i = 0; i < n; ++i) {
        float d = a[i] - b[i];
        sum += d * d;
    }
    return sum;
}
Hand-Tuned (AVX-512)
float sum_sq_diff_avx512(
    const float* __restrict__ a,
    const float* __restrict__ b,
    size_t n)
{
    __m512 vsum = _mm512_setzero_ps();
    size_t i = 0;
    for (; i + 16 <= n; i += 16) {
        __m512 va = _mm512_loadu_ps(a + i);
        __m512 vb = _mm512_loadu_ps(b + i);
        __m512 vd = _mm512_sub_ps(va, vb);
        vsum = _mm512_fmadd_ps(vd, vd, vsum);
    }
    float result = _mm512_reduce_add_ps(vsum);
    for (; i < n; ++i) {
        float d = a[i] - b[i];
        result += d * d;
    }
    return result;
}

Now, to be fair: the AI-generated scalar loop can auto-vectorize under -O2 -march=native. But it usually does not vectorize optimally, for several reasons:

  • Missing __restrict__. Without the aliasing hint, the compiler must assume a and b might overlap. This blocks vectorization or forces the compiler to emit a runtime alias check with a scalar fallback.
  • No FMA exploitation. The hand-tuned version uses fused multiply-add (_mm512_fmadd_ps), which computes d*d + sum in a single instruction with higher precision. Auto-vectorizers may not emit FMA unless -ffp-contract=fast is explicitly enabled.
  • Reduction pattern. The scalar sum += d * d creates a loop-carried dependency. The AVX-512 version accumulates into a 512-bit register, deferring the horizontal reduction to the end.

Benchmark results (1M float pairs, AMD Ryzen 9 7950X, AVX-512)

ImplementationThroughputCycles/ElementIPC
AI-generated (scalar, no auto-vec)890 MB/s4.21.1
AI-generated (with -O2 -march=native)5.2 GB/s0.722.8
Hand-tuned AVX-512 intrinsics14.8 GB/s0.253.9
Delta (auto-vec vs intrinsics)2.85x

Even when the compiler auto-vectorizes the AI-generated code, the hand-tuned intrinsics version is 2.85x faster. This is the gap between "the compiler did something" and "a human understood the microarchitecture." For a kernel called billions of times in a signal processing pipeline, that gap is the difference between meeting a latency SLA and missing it.


6. Benchmark: Cache-Aware Data Layout

This is where LLMs fail most silently. The generated code is clean, idiomatic, and completely wrong for the hardware.

Task: iterate over 1M entities, updating position based on velocity. A standard game/simulation tick.

AI-Generated (AoS)
struct Entity {
    glm::vec3 position;
    glm::vec3 velocity;
    float health;
    uint32_t id;
    std::string name;
    uint64_t last_update;
    uint32_t flags;
    // 88+ bytes per entity
};

std::vector<Entity> entities;

for (auto& e : entities) {
    e.position += e.velocity * dt;
}
Hand-Tuned (SoA)
struct Entities {
    std::vector<glm::vec3> pos;
    std::vector<glm::vec3> vel;
    std::vector<float> health;
    std::vector<uint32_t> id;
    std::vector<std::string> name;
    std::vector<uint64_t> last_update;
    std::vector<uint32_t> flags;
};

Entities ents;

const size_t n = ents.pos.size();
for (size_t i = 0; i < n; ++i) {
    ents.pos[i] += ents.vel[i] * dt;
}

Why the AoS layout is slow

Each Entity is 88+ bytes. A 64-byte cache line fits zero complete entities. The hot loop only touches position (12 bytes) and velocity (12 bytes) — 24 bytes out of 88+. That means 73% of every cache line loaded is wasted on fields the loop never reads: health, id, name, last_update, flags.

The SoA layout places all positions contiguously and all velocities contiguously. Every byte loaded into cache is a byte the loop actually uses. The hardware prefetcher detects the sequential access pattern and stays ahead of the computation.

Benchmark results (1M entities, AMD Ryzen 9 7950X)

LayoutTimeL1d MissesL2 MissesEffective BW
AI-generated AoS4.8 ms2.1M890K2.6 GB/s
Hand-tuned SoA0.42 ms48K12K22.8 GB/s
Delta11.4x43.7x74.2x8.8x

Every LLM I tested generated the AoS layout. Every single one. This is not a bug — it is a training data problem. AoS is what you see in textbooks, tutorials, and the vast majority of GitHub repositories. SoA is what you see in game engines, HFT systems, and physics simulations. The training data teaches correctness and readability. The hardware rewards layout and locality.


7. Benchmark: Memory Allocator Hot Path

I asked LLMs to optimize a function that processes a stream of variable-length network packets. The AI-generated approach consistently used std::vector or std::make_unique for per-packet buffer allocation.

// AI-generated: allocates per packet
void process_packets(PacketStream& stream) {
    while (auto pkt = stream.next()) {
        auto buf = std::make_unique<uint8_t[]>(pkt->len);  // heap alloc per packet
        decode(pkt->data, buf.get(), pkt->len);
        dispatch(buf.get(), pkt->len);
    }
}
// Hand-tuned: arena allocator, zero syscalls in steady state
void process_packets(PacketStream& stream) {
    // 2MB arena, reset per batch — no malloc, no free
    std::array<std::byte, 1 << 21> arena_storage;
    std::pmr::monotonic_buffer_resource arena{
        arena_storage.data(), arena_storage.size()};

    while (auto pkt = stream.next()) {
        auto* buf = static_cast<uint8_t*>(
            arena.allocate(pkt->len, 16));  // pointer bump, ~1ns
        decode(pkt->data, buf, pkt->len);
        dispatch(buf, pkt->len);
    }
}

Benchmark results (10M packets, avg 256 bytes each)

ApproachAlloc Latency (p50)Alloc Latency (p99)Total Throughput
AI-generated (make_unique)48 ns2,100 ns6.2M pkt/s
Hand-tuned (PMR arena)1.2 ns8 ns42M pkt/s
Delta40x262x6.8x

The p99 number is what matters in production. The 2,100 ns tail on make_unique is not malloc being slow on average — it is malloc occasionally hitting a page fault, a lock contention event, or a coalesce operation in the allocator. The arena approach eliminates the entire category of problem. Its p99 is 8 ns because the worst case is a pointer bump that crosses a cache line.


8. Profiler Analysis: The Common Failure Patterns

Across all four benchmarks, the AI-generated code exhibits the same categories of failure under perf stat:

Failure ModeRoot CauseTypical Perf Impact
Excessive cache missesAoS layout, pointer chasing, no prefetch hints5-40x slowdown
Branch mispredictionsGeneric control flow, no [[likely]]/[[unlikely]], no branchless patterns1.5-3x slowdown
Unnecessary heap allocationsPer-element new, no arena/pool, no stack buffers3-10x slowdown, tail latency spikes
Memory ordering overkillDefault seq_cst everywhere, no acquire/release reasoning2-5x on atomic-heavy code
Missed vectorizationNo __restrict__, loop-carried dependencies, no intrinsics2-16x (depends on SIMD width)
Poor IPC utilizationInstruction-level parallelism not exploited, unnecessary serialization1.5-4x

None of these failures are bugs. The AI-generated code passes every test. It produces correct results. It compiles cleanly. The failure is invisible unless you have a profiler open. And that is precisely the problem — in a world of "vibe coding" and AI-generated pull requests, who is opening the profiler?


9. Where AI Genuinely Excels

This is not a Luddite argument. AI tools provide real, measurable value in performance engineering — just not on the hot path.

Test generation

LLMs are excellent at generating property-based tests, fuzz harnesses, and edge-case test matrices for lock-free data structures. Writing a comprehensive test suite for a SPSC queue is tedious, error-prone work that AI handles well. I estimate AI reduces my test-writing time by 60-70% with minimal review overhead.

Boilerplate and glue code

Serialization, logging wrappers, CLI argument parsing, build system configuration — none of this is on the hot path, and AI generates it reliably. It frees up time to focus on the 2% of code that determines system performance.

Documentation and code review prep

Explaining why a lock-free implementation uses memory_order_acquire instead of memory_order_seq_cst is something LLMs do well. Generating inline documentation for complex SIMD intrinsics pipelines is genuinely useful. The irony is that AI is better at explaining performance-critical code than writing it.

Prototyping and exploration

When I need to evaluate whether a problem is amenable to a particular approach — "would a B-tree or a skip list work better here?" — AI-generated prototypes give me a working starting point in minutes instead of hours. The prototype is never what ships, but it accelerates the decision.

Refactoring cold paths

Moving a codebase from C++17 to C++23 idioms, replacing raw pointers with std::expected, modernizing error handling — AI handles these mechanical transformations well when the code is not performance-sensitive.


10. Prompt Engineering for Performance-Critical Code

Can you make LLMs generate better performance code with better prompts? Partially. Here is what I have found actually works, and what does not.

What works

  • Specifying the hardware target. "Target x86-64 with AVX-512, 64-byte cache lines, 32KB L1d" produces measurably better output than generic prompts. The model draws on architecture-specific training data instead of defaulting to portable but slow patterns.
  • Specifying constraints explicitly. "Zero heap allocations in the hot path," "use memory_order_acquire/release, not seq_cst," or "SoA layout, not AoS" steers the model toward the right design. But you need to already know the right design to prompt for it.
  • Asking for multiple implementations ranked by latency. "Give me three implementations: one optimizing for readability, one for throughput, one for p99 latency" surfaces options the model would not generate by default.
  • Providing perf stat output. Pasting profiler output and asking "what is causing these cache misses?" produces useful diagnostic reasoning, even if the suggested fix still needs manual tuning.

What does not work

  • "Make it fast." This produces micro-optimizations that do not address the fundamental design — reserve() calls, std::move in obvious places, noexcept annotations. Surface-level changes that show up in diffs but not in profiler output.
  • "Optimize for cache performance." Without specifying the data layout, the model adds __builtin_prefetch calls to fundamentally cache-hostile code. Prefetching a linked list does not fix the problem of pointer chasing.
  • Iterative refinement through conversation. Each round of "now make it faster" produces diminishing returns. The model reshuffles the same patterns instead of rethinking the fundamental approach. After three iterations, you are doing the engineering yourself and using the LLM as a typist.

The core problem: effective prompting for performance code requires the same expertise as writing the performance code. The value proposition of AI — enabling people who lack expertise to produce expert-level output — breaks down precisely where expertise is most needed.


Takeaways

Hinkelmann is right that the industry is shifting. The question is not whether AI changes engineering — it already has. The question is where the boundary lies between "AI-generated is good enough" and "AI-generated is a liability."

  1. AI-generated code is correct code, not fast code. Every benchmark shows the same pattern: logically correct, architecturally naive. For 95% of software, correct is sufficient. For the remaining 5%, the gap is 3-15x.
  2. The failure is invisible without instrumentation. AI-generated code passes tests, compiles cleanly, and produces correct output. The performance regression only shows up in production under load, when the profiler reveals the cache misses, the tail latency spikes, and the unnecessary allocations.
  3. AI excels at everything except the hot path. Test generation, boilerplate, documentation, prototyping, cold path refactoring — use AI aggressively here. It saves real time with low risk.
  4. Prompting is not a substitute for expertise. You can coax better output from LLMs with hardware-specific prompts and explicit constraints. But you need to already know what to ask for, which means the expertise has shifted, not disappeared.
  5. The new skill stack is performance engineering + AI fluency. The engineers who will thrive are those who use AI to eliminate the 80% of work that is not performance-sensitive, freeing time to focus on the 20% that is. The skill is knowing which is which.
  6. Measure everything. If you are accepting AI-generated code into a performance-sensitive codebase, mandate profiler output in the review process. Not just "it passes tests" — "here are the cache miss rates, here is the IPC, here is the p99 under load."

The era of orchestrating intent is real. But intent does not specify cache line alignment. Intent does not choose memory ordering. Intent does not lay out data for the prefetcher. Until AI models have a hardware feedback loop — until they can run perf stat on their own output and iterate — performance engineering remains a human discipline augmented by AI, not replaced by it.


Sources & Further Reading


← cd ~