← 返回 FEED
GITHUB2026-04-21

用纯 x86-64 汇编写一个神经网络

核心内容

用纯 x86-64 汇编实现了一个能解决 XOR 问题的双层神经网络(2 → 2 → 1 架构)。网络虽小,但包含了现代神经网络的全部核心组件:前向传播、激活函数、损失计算、反向传播、权重更新和训练循环。零外部依赖,只用 NASM 汇编器和 GCC 连接。

网络结构

输入层(2 个神经元)
       ↓
隐藏层(2 个神经元,Sigmoid 激活)
       ↓
输出层(1 个神经元,Sigmoid 激活)

XOR 问题的关键在于:单层感知机无法解决它,必须至少有一个隐藏层。这篇文章把这件事用最底层的方式呈现出来。

内存布局:没有数组,只有手动的地址管理

在高级语言里,一个浮点数数组就是一个变量。在汇编里,每个权重、偏置、输入和中间值都有自己标签化的内存位置。训练数据以平坦布局存储:4 个样本 × 3 个浮点数 = 12 个浮点数 = 48 字节,用指针算术逐个遍历。

section .data
    train_data:
        dd 0.0, 0.0, 0.0      ; 0 XOR 0 = 0
        dd 0.0, 1.0, 1.0      ; 0 XOR 1 = 1
        dd 1.0, 0.0, 1.0      ; 1 XOR 0 = 1
        dd 1.0, 1.0, 0.0      ; 1 XOR 1 = 0

section .bss
    z1:     resd 1    ; 隐藏神经元 1 的预激活值
    h1:     resd 1    ; 隐藏神经元 1 的激活后值
    output: resd 1    ; 最终输出

dd(define double-word)分配 4 字节;resd(reserve)预留空间但不清零——在 Python 里这些操作一个列表就搞定了。

Sigmoid:一个 CPU 指令也给你省了

Sigmoid 函数 σ(x) = 1 / (1 + e^(-x))。关键问题是:汇编没有 exp() 指令。

作者用了 x87 FPU(自 8087 以来就存在的浮点运算单元,基于栈的架构):利用 f2xm1(计算 2^x - 1)和 fscale(乘以 2^整数部分),配合恒等式 e^x = 2^(x × log₂(e)),手工构建了 exp 函数。整个 Sigmoid 实现花了约 30 行汇编,包含 FPU 栈操作、舍入、分数/整数分离。

这就是"一行 Python 代码背后是什么"的极致版本。

寄存器陷阱:函数调用会破坏你的值

这是神经网络汇编实现中最容易踩的坑。调用 Sigmoid 函数会破坏 xmm0-xmm7(System V AMD64 调用规范规定这些都是调用者保存寄存器)。在 Python 里,h1 = sigmoid(...) 之后 x1 还是 x1;在汇编里,调用函数后 x1 可能已经被覆盖了。

解法:把输入保存到 .bss 段,在调用函数之前重新加载。

forward_pass:
    ; 在调用 sigmoid 之前必须先保存 x1, x2
    movss [save_x1], xmm0
    movss [save_x2], xmm1
    ; ...
    call sigmoid
    ; sigmoid 返回后 xmm0 是 h1,需要重新加载原始输入
    movss xmm0, [save_x1]   ; 重新加载 x1
    movss xmm1, [save_x2]   ; 重新加载 x2

反向传播:链式法则的机械执行

反向传播在汇编里的实现完全展示了链式法则的执行路径——梯度逐个乘回来:

; 输出层 delta
; d_out = error * sigmoid'(output)
; sigmoid'(x) = σ(x) * (1 - σ(x))
movss xmm1, [output]
movss xmm2, [one]
subss xmm2, xmm1        ; xmm2 = 1 - output
mulss xmm1, xmm2        ; xmm1 = output * (1 - output)
mulss xmm0, xmm1        ; xmm0 = error * sigmoid' = d_out

每一行对应一个物理的乘/减操作。没有 autograd,没有计算图,只有直接的算术。

训练循环:指针算术的枯燥之美

主循环遍历每个 epoch,再遍历每个样本,逐个完成前向传播 → 计算误差 → 反向传播 → 更新权重:

.epoch_loop:
    ; ...
.sample_loop:
    movss xmm0, [r14]       ; x1(r14 是训练数据指针)
    movss [save_x1], xmm0
    movss xmm0, [r14 + 4]   ; x2
    movss [save_x2], xmm0
    call forward_pass
    movss xmm0, [r14 + 8]   ; target
    call compute_error
    call backprop
    call update_weights
    add r14, 12              ; 移动到下一个样本(3 浮点 × 4 字节)
    inc r13
    jmp .sample_loop

10000 个 epoch 后,输出接近 XOR 真值表(0.0432、0.9541、0.9538、0.0517)。

价值在哪里

这篇文章的核心价值不是"教你写神经网络"——这是最小的神经网络。它的价值在于让你看到:autograd 消除的不是计算复杂度,而是管理这整堆寄存器、内存地址和调用约定的心理负担。当你理解了梯度在汇编层如何逐个乘回来,就再也不会觉得"神经网络很神秘"了。

作者也给出了几个继续探索的方向:用 ReLU 替代 Sigmoid(实现更简单,训练更快)、使用 SIMD 指令一次处理多个神经元、用随机数生成器替代手工初始权重。