Zig SIMD in assembly

Tue 14 July 2026

In the C ABI post we saw how convenient it's to write Intel-style assembly code in Zig. This time let's see what it takes to use SIMD in assembly.

Assembly side

The first SIMD example in Modern x86 Assembly book demonstrates 16-bit integers addition using both wraparound and saturated arithmetic.

[10,   200,     30, -32766,  50,   60, 32000,  -32000] xmm0
                                                    +
[100, -200,  32760,   -400, 500, -600,  1200,    -950] xmm1
                                                    =
[110,    0, -32746,  32370, 550, -540, -32336,  32586] xmm2 (wraparound)
[110,    0,  32767, -32768, 550, -540,  32767, -32768] xmm3 (saturated)
   0     1       2       3    4     5       6       7  lane

Wraparound is what we normally expect to happen (arithmetic overflow/underflow):

  • lanes 0, 1, 4, and 5 stayed within boundaries
  • lanes 2 and 6 overflowed the maximum capacity of an i16 (32,767)
  • lanes 3 and 7 underflowed past the minimum capacity (-32,768)

With saturated arithmetic, the results are clipped to the maximum capacity:

  • lanes 2 and 6 are capped at 32,767
  • lanes 3 and 7 are capped at -32,768

Here is an assembly code that LLVM understands llvm/ch08_01.s. As we can see, a C++ call AddI16_avx(&c1, &c2, &a, &b) of the assembly function supplies 4 pointers:

  • &a and &b are the inputs
  • &c1 and &c2 are the outputs

In essence, the function sums the input vectors with wraparound/saturated arithmetic and stores the resulting vectors in &c1 and &c2 respectively.

.intel_syntax noprefix
.text
.global _AddI16_avx

# Calculates *c1 = *a + *b (wraparound), *c2 = *a + *b (saturated).
# AddI16_avx(c1: *XmmVal, c2: *XmmVal, a: *const XmmVal, b: *const XmmVal) void;
_AddI16_avx:
    vmovdqa xmm0, [rdx]         # xmm0 = *a
    vmovdqa xmm1, [rcx]         # xmm1 = *b

    vpaddw xmm2, xmm0, xmm1     # xmm2 = xmm0 + xmm1 (wraparound)
    vpaddsw xmm3, xmm0, xmm1    # xmm3 = xmm0 + xmm1 (saturated)

    vmovdqa [rdi], xmm2         # *c1 = xmm2
    vmovdqa [rsi], xmm3         # *c2 = xmm3
    ret

Since those pointers are basically 64-bit unsigned integers holding memory addresses, they are passed in rdi (c1), rsi (c2), rdx (a), rcx (b) registers in accordance with the C ABI.

The first instruction vmovdqa xmm0, [rdx] copies data from a memory address stored in a general purpose register rdx (a) to a vector register xmm0. The instruction name stands for Vector Extension (VEX) MOVe Double Quadword Aligned. The eight i16 items (128 bits total or a double quadword) [10, 200, 30, -32766, 50, 60, 32000, -32000] are copied from an aligned memory address stored in rdx to xmm0.

Aligned means starting at a memory address that is a multiple of the vector's size in bytes. That means the variable &a (memory address) must be aligned to 16 bytes for XMM register (128 bits / 8 bits = 16 bytes). Note, there is vmovdqu instruction that works with unaligned addresses, see Intro to SIMD in avo.

The second instruction vmovdqa xmm1, [rcx] is similar, i.e., it copies from a memory address &b held in rcx register to xmm1 like this xmm1 = *rcx, i.e., xmm1 = [100, -200, 32760, -400, 500, -600, 1200, -950].

The third and fourth instructions vpaddw xmm2, xmm0, xmm1 and vpaddsw xmm3, xmm0, xmm1 represent wraparound and saturated additions:

  • Vector (VEX) Packed ADD Word: 16-bit elements of vectors xmm0 and xmm1 are added and the result is stored in xmm2 = [110, 0, -32746, 32370, 550, -540, -32336, 32586]
  • Vector (VEX) Packed ADD Saturated Word: the same 16-bit elements are added and the result is stored in xmm3 = [110, 0, 32767, -32768, 550, -540, 32767, -32768]

The last instructions vmovdqa [rdi], xmm2 and vmovdqa [rsi], xmm3 copy the computed results from xmm2/xmm3 vector registers to memory addresses held in rdi/rsi registers (&c1 and &c2 variables on the caller side).

C++ side

In the C++ example, XmmVal type represents all sorts of 128-bit values we could place into XMM registers, e.g., four floats, eight 16-bit integers, etc. It is an anonymous union defined within a struct, allowing its members to be accessed directly as if they were part of the struct. The alignas(16) specifier tells the compiler to place XmmVal variables/constants at a memory address that is a multiple of 16 bytes (vmovdqa instruction requires this, we talked about it above).

struct alignas(16) XmmVal {
    union {
        int8_t   m_I8[16];
        int16_t  m_I16[8];
        int32_t  m_I32[4];
        int64_t  m_I64[2];
        uint8_t  m_U8[16];
        uint16_t m_U16[8];
        uint32_t m_U32[4];
        uint64_t m_U64[2];
        float    m_F32[4];
        double   m_F64[2];
    };
};

Since we're dealing with 16-bit integers, we must use int16_t m_I16[8] field in our cpp/ch08_01.cpp program.

extern "C" void AddI16_avx(XmmVal* c1, XmmVal* c2, const XmmVal* a, const XmmVal* b);

int main() {
    XmmVal c1, c2;
    const XmmVal a = { .m_I16 = { 10, 200, 30, -32766, 50, 60, 32000, -32000 } };
    const XmmVal b = { .m_I16 = { 100, -200, 32760, -400, 500, -600, 1200, -950 } };

    AddI16_avx(&c1, &c2, &a, &b);
    // Print the results...

    return 0;
}

The results of wraparound c1 and saturated c2 addition match the expectations.

﹩ zig c++ ./cpp/ch08_01.cpp ./llvm/ch08_01.s -o main && ./main
C++ says
c1={ 110, 0, -32746, 32370, 550, -540, -32336, 32586 }
c2={ 110, 0, 32767, -32768, 550, -540, 32767, -32768 }

🦉 Note, C++ has C-compatible struct layout as long as there are no virtual functions. If XmmVal had one, C++ compiler would have injected a hidden pointer to a vtable at the beginning of the struct 😰:

  • vmovdqa xmm0, [rdx] would read _vptr (8 bytes) and half of m_I16 (8 bytes)
  • XmmVal size would increase from 16 bytes to 32 bytes: 8 bytes pointer, 16 bytes of m_I16, and 8 bytes of trailing padding. The compiler adds 8 bytes to make sure the total struct size is a multiple of 16 (24 bytes of _vptr and m_I16 are not divisible by 16, but 32 is) in case XmmVal is used in an array, e.g., XmmVal x[2];.
  • vpaddw xmm2, xmm0, xmm1 would sum a garbage data
  • vmovdqa [rdi], xmm2 would produce a corrupted c1, e.g., its _vptr would be broken because we would copy xmm2 there
struct alignas(16) XmmVal {
    void*   _vptr;        // 8 bytes
    int16_t m_I16[8];     // 16 bytes
    char    __padding[8]; // 8 bytes
};

Zig side

Zig code zig/ch08_01.zig isn't very different from C++ one: we'got XmmVal struct wrapping a union, it also has a 16-byte alignment requirement.

Note, const XmmVal = extern struct { ... } has extern keyword that instructs Zig compiler to keep the stable memory layout.

const XmmVal = extern struct {
    data: extern union {
        m_I8: [16]i8,
        m_I16: [8]i16,
        m_I32: [4]i32,
        m_I64: [2]i64,
        m_U8: [16]u8,
        m_U16: [8]u16,
        m_U32: [4]u32,
        m_U64: [2]u64,
        m_F32: [4]f32,
        m_F64: [2]f64,
    } align(16),
};

We use m_I16: [8]i16 field to set an array of 16-bit integers just like in the C++ example.

extern fn AddI16_avx(c1: *XmmVal, c2: *XmmVal, a: *const XmmVal, b: *const XmmVal) void;

pub fn main() void {
    const a = XmmVal{
        .data = .{
            .m_I16 = [8]i16{ 10, 200, 30, -32766, 50, 60, 32000, -32000 },
        },
    };
    const b = XmmVal{
        .data = .{
            .m_I16 = [8]i16{ 100, -200, 32760, -400, 500, -600, 1200, -950 },
        },
    };
    var c1 = XmmVal{ .data = .{ .m_I16 = undefined } };
    var c2 = XmmVal{ .data = .{ .m_I16 = undefined } };

    AddI16_avx(&c1, &c2, &a, &b);
    // Print the results...
}

The results match C++ output.

﹩ zig run ./zig/ch08_01.zig ./llvm/ch08_01.s
Zig says
c1={ 110, 0, -32746, 32370, 550, -540, -32336, 32586 }
c2={ 110, 0, 32767, -32768, 550, -540, 32767, -32768 }

Even though the compilers can auto-vectorize code, it's good to know how to write SIMD in assembly. Calling such functions from C++/Zig isn't always straightforward due to alignment requirements.

Category: Zig Tagged: assembler zig simd

comments


Hello World in avo 🥑

Tue 02 December 2025

Let's learn together how to write some Go assembly using avo aka writing assembly-like Go code to generate assembly. To make it more clear, here is an avo program add/asm.go.

package main

import asm "github.com/mmcloughlin/avo/build"

func main() {
    asm.TEXT("Add", asm.NOSPLIT, "func(x …

Category: Go Tagged: assembler golang

comments

Read More

Linux process

Tue 31 January 2023

Being curious about BPF, I studied source code of several programs from the BCC libbpf-tools. BPF performance tools book aided me to navigate BPF C code. For example, it explained that a BPF program has to use helpers because it can't access arbitrary memory (outside of BPF) and can't call …

Category: Infrastructure Tagged: architecture linux

comments

Read More

API based on Flask

Mon 09 December 2013

Here I want to consider implementation of API best practices which usually don't follow Fielding's REST strictly. Example Flask project is on GitHub.

API Versioning

Interfaces are changed hence versioning is mandatory in order to not annoy your users. You might need to add new resource or field to particular …

Category: Python Tagged: python flask api

comments

Read More

Preparation to Python Interview

Fri 02 November 2012

I decided to collect a little more information and experience during preparation to Python developer interview. These are some information and links which seemed important to me. Maybe it will be helpful.

How does it usually go?

What kind of projects did you participate in?

What did you do at …

Category: Python Tagged: python interview

comments

Read More

Django TODO: тестирование во время конструирования

Fri 29 June 2012

Тестирование, выполняемое разработчиками -- один из важнейших элементов полной стратегии тестирования.

Тестирование может указать только на отдельные дефектные области программы -- оно не сделает программу удобнее в использовании, более быстрой, компактной, удобочитаемой или расширяемой.

Цель тестирования противоположна целям других этапов разработки. Его целью является нахождение ошибок. Успешным считается тест, нарушающий работу ПО …

Category: Python Tagged: python django django-todo testing

comments

Read More

Django TODO: конструирование системы

Fri 29 June 2012

При работе над проектом конструирование включает другие процессы, в том числе проектирование. Формальная архитектура дает ответы только на вопросы системного уровня, при этом значительная часть проектирования может быть намеренно оставлена на этап конструирования. Проектирование -- это "постепенный" процесс. Проекты приложений не возникают в умах разработчиков сразу в готовом виде. Они развиваются …

Category: Python Tagged: python django django-todo construction

comments

Read More

Django TODO: проектирование архитектуры системы

Fri 29 June 2012

Следующим этапом разработки системы является проектирование архитектуры.

Архитектура должна быть продуманным концептуальным целым. Главный тезис самой популярной книги по разработке ПО "Мифический человеко-месяц" гласит, что основной проблемой, характерной для крупных систем, является поддержание их концептуальной целостности. Хорошая архитектура должна соответствовать проблеме [1].

Разделение системы на подсистемы на уровне архитектуры, позволяет …

Category: Python Tagged: python django django-todo architecture

comments

Read More

Django TODO: выработка требований к системе

Fri 29 June 2012

После прочтения Макконелла захотелось спроецировать его советы на Django. Для этого я взял за основу разработку системы Django TODO. Итак, первый этап -- выработка требований к системе.

Требования подробно описывают, что должна делать система. Внимание к требованиям помогает свести к минимуму изменения системы после начала разработки. Явные требования помогают гарантировать, что …

Category: Python Tagged: python django django-todo requirements

comments

Read More

Соглашения по разработке на Python/Django

Fri 29 June 2012

Во время разработки я часто сверяюсь с известными мне соглашениями, стараюсь следовать рекомендациям. Цитировать их не имеет смысла -- лучше приведу ссылки.

PEP 8 -- Style Guide for Python Code.

Code Like a Pythonista: Idiomatic Python. В нем я нашел ответы на вопросы форматирования длинных строк:

expended_time = (self.finish_date() - self.start_date
                 + datetime …

Category: Python Tagged: python django best practices

comments

Read More

Разделение настроек в Django

Fri 29 June 2012

В Django wiki собраны различные способы разделения настроек. Мне нравится вариант, описанный в блоге Senko Rašić:

settings/
├── __init__.py
├── base.py
├── development.py
├── local.py
└── production.py

base.py содержит общие настройки для development.py и production.py, например:

ADMINS = ()
MANAGERS = ADMINS

TIME_ZONE = 'Asia/Yekaterinburg'
# ...

production.py содержит настройки для …

Category: Python Tagged: python django settings

comments

Read More

Краткий обзор инфраструктуры для разработки reusable Django приложений

Wed 13 June 2012

Начиная впервые разрабатывать веб-приложения на новом фреймворке программист зачастую сталкивается с некоторыми трудностями. При разработке отчуждаемых веб-приложений на Django к этим проблемам необходимо отнести организацию файлов в проекте, обнаружение тестов, вопросы пакетирования приложений и организации автоматизированного тестирования. В данной статье приведены пути решения этих проблем.

Важно знать различия между двумя …

Category: Python Tagged: python django infrastructure

comments

Read More

Вычислительные методы одномерной оптимизации

Wed 06 October 2010

На третьем курсе по предмету методы оптимизации делали лабораторную работу на тему «Вычислительные методы одномерной оптимизации». Задача заключалась в поиске безусловного минимума функции f(x) = pow(x, 3) – x + pow(e, -x) на начальном интервале [0, 1] с точностью 0.00001.

Вычисления производились через:

  • пассивный метод;
  • равномерные блочные методы;
  • метод …

Category: Misc Tagged: php mathematical optimization

comments

Read More

Определение нажатия комбинации клавиш средствами BIOS на ассемблере

Thu 03 December 2009

По учебе понадобилось написать программу на ассемблере, которая должна распознать нажатие «горячей» комбинации клавиш LeftCtrl+RightShift+F3 и реагировать на него звуковым сигналом. Информации/примеров по этой теме маловато, по этому решил опубликовать свою программку.

masm
.model small
.stack 256
.data
    Msg_about db 'Распознать нажатие «горячей» комбинации клавиш', 0Ah, 0Dh …

Category: Misc Tagged: assembler

comments

Read More

Моделирование одноканальной СМО с отказами

Sat 30 May 2009

Дана одноканальная система массового обслуживания с отказами. В нее поступают заявки через промежуток времени n, где n – случайная величина, подчиненная равномерному закону распределения. Время обслуживания заявки системой m также является случайной величиной с показательным законом распределения. Если к моменту прихода заявки канал занят, заявка покидает систему необслуженной.

Изначально код был …

Category: Misc Tagged: python modeling single-channel queue

comments

Read More