• Home
  • About
    • 康青旭 - Germán Caggianese photo

      康青旭 - Germán Caggianese

      Refactoring entropy in my Mind

    • Learn More
    • Email
    • Instagram
    • Github
    • Codeberg   Codeberg
  • All Posts
  • Projects
  • Areas
  • Resources

Advent of Code 2025 - Dia 4 en flat assembler

03 Jan 2026

Reading time ~44 minutes

“Si podés optimizar el trabajo que hacen los montacargas, quizá tengan tiempo de sobra para atravesar la pared.”

Traducción de la publicación original en inglés.

Github: GCaggianese/AoC-2025/D4

  • flat assembler

Puzzle Dia 4: Printing Department

Parte uno:

  • Input de matriz donde . = rollo de papel, @ = espacio vacío
  • Válida = espacio vacío con 5+ rollos de papel adyacentes
  • Devolver la cantidad de posiciones válidas

Parte dos:

  • Propagación: las posiciones válidas se convierten en rollos de papel
  • Repetir hasta que no aparezcan nuevas posiciones válidas
  • Devolver el conteo total de todas las posiciones activadas

Incluí una implementación en Python de la solución que es bastante directa de entender.

Implementación en FASM

Licencia

;;; SPDX-FileCopyrightText: 2026 Germán Caggianese <german.caggianese@pm.me>
;;;
;;; SPDX-License-Identifier: Apache-2.0

Encabezado

format ELF64 executable
entry _start

Macro de matriz

Esta macro parsea el archivo de input en tiempo de compilación, convirtiendo:

  • . (rollo de papel) → 1
  • @ (espacio vacío) → 0
  • Los saltos de línea y otros caracteres se omiten

El bloque virtual carga el archivo en un espacio de direccionamiento, y luego iteramos cada byte y emitimos solo los significativos.

macro matrix_from_file name, filename {
    virtual at 0
        name#.data:: file filename
        name#.size = $
    end virtual
    label name byte
    name#.len = 0
    repeat name#.size
        load name#.ch byte from name#.data:(%-1)
        if name#.ch = '.'
            db 1
            name#.len = name#.len + 1
        else if name#.ch = '@'
            db 0
            name#.len = name#.len + 1
        end if
    end repeat
}

Segmento de datos

Cargar el input y definir constantes

segment readable writeable
    matrix_from_file my_matrix, './input.txt'
    MATRIX_SIZE = my_matrix.len
    ROWS = 138
    COLS = 138

Buffers de trabajo

  • result: guarda las sumas de vecinos después de cada cálculo
  • backup: preserva el estado original de la matriz para la parte 2
result rb ROWS * COLS
backup rb ROWS * COLS

Offsets de dirección para conectividad de 8 vecinos

Cada par es (row~offset~, col~offset~) como bytes con signo.

directions db -1,-1, -1,0, -1,1, 0,-1, 0,1, 1,-1, 1,0, 1,1
NUM_DIRS = 8

Contadores y strings de salida

    counter_p1 dq 0
    counter_p2 dq 0

    msg_p1 db 'Counter P1: '
    msg_p1_len = $ - msg_p1
    msg_p2 db 'Counter P2: '
    msg_p2_len = $ - msg_p2
    buffer rb 16
    newline db 0x0A

segment readable executable

Lógica principal

_start:

Backup de la matriz original

Necesitamos preservar el estado original porque la parte 2 lo va a modificar.

  • Copiar my~matrix~ → backup
    xor rcx, rcx
.backup_loop:
    cmp rcx, MATRIX_SIZE
    jge .part1_start
    mov al, [my_matrix + rcx]
    mov [backup + rcx], al
    inc rcx
    jmp .backup_loop

Parte 1: cálculo de una sola pasada

Sumar vecinos

Para cada celda, sumar su valor más todos los vecinos válidos. Nota:

  • r12 = índice de fila
  • r13 = índice de columna
.part1_start:
    xor r12, r12

.p1_row_loop:
    cmp r12, ROWS
    jge .p1_correction

    xor r13, r13

.p1_col_loop:
    cmp r13, COLS
    jge .p1_next_row

Calcular el índice lineal: rax = row * COLS + col. Nota:

  • r14 = suma (empieza con el valor de la celda)
  • r15 = índice de dirección
    mov rax, r12
    imul rax, COLS
    add rax, r13
    movzx r14d, byte [my_matrix + rax]

    xor r15, r15

.p1_dir_loop:
    cmp r15, NUM_DIRS
    jge .p1_store_result

Cargar offsets de dirección


movsx r8, byte [directions + r15*2]      ; r8 = di
movsx r9, byte [directions + r15*2 + 1]  ; r9 = dj

Calcular la posición del vecino. Nota:

  • r10 = ni = row + di
  • r11 = nj = col + dj

mov r10, r12
add r10, r8
mov r11, r13
add r11, r9

Chequeo de límites: 0 <= ni < ROWS && 0 <= nj < COLS

cmp r10, 0
jl .p1_next_dir
cmp r10, ROWS
jge .p1_next_dir
cmp r11, 0
jl .p1_next_dir
cmp r11, COLS
jge .p1_next_dir

Sumar el valor del vecino a la suma

    mov rax, r10
    imul rax, COLS
    add rax, r11
    movzx eax, byte [my_matrix + rax]
    add r14, rax

.p1_next_dir:
    inc r15
    jmp .p1_dir_loop

.p1_store_result:
    mov rax, r12
    imul rax, COLS
    add rax, r13
    mov byte [result + rax], r14b

    inc r13
    jmp .p1_col_loop

.p1_next_row:
    inc r12
    jmp .p1_row_loop

Corrección

Las celdas de borde tienen menos vecinos, así que compensamos:

  • Celdas de borde (no esquina): +3
  • Celdas de esquina: +3 +2 = +5

Esto normaliza todas las celdas como si estuvieran rodeadas por espacio vacío.

Fila superior: +3

.p1_correction:
    xor rcx, rcx
.p1_top_row:
    cmp rcx, COLS
    jge .p1_bottom_row
    add byte [result + rcx], 3
    inc rcx
    jmp .p1_top_row

Fila inferior: +3


.p1_bottom_row:
    xor rcx, rcx
.p1_bottom_loop:
    cmp rcx, COLS
    jge .p1_left_col
    mov rax, (ROWS-1) * COLS
    add rax, rcx
    add byte [result + rax], 3
    inc rcx
    jmp .p1_bottom_loop

Columna izquierda (excluyendo esquinas): +3

.p1_left_col:
    mov rcx, 1
.p1_left_loop:
    cmp rcx, ROWS-1
    jge .p1_right_col
    mov rax, rcx
    imul rax, COLS
    add byte [result + rax], 3
    inc rcx
    jmp .p1_left_loop

Columna derecha (excluyendo esquinas): +3

.p1_right_col:
    mov rcx, 1
.p1_right_loop:
    cmp rcx, ROWS-1
    jge .p1_corners
    mov rax, rcx
    imul rax, COLS
    add rax, COLS-1
    add byte [result + rax], 3
    inc rcx
    jmp .p1_right_loop

Esquinas: +2 adicional

.p1_corners:
    add byte [result], 2                              ; [0][0]
    add byte [result + COLS - 1], 2                   ; [0][COLS-1]
    add byte [result + (ROWS-1) * COLS], 2            ; [ROWS-1][0]
    add byte [result + (ROWS-1) * COLS + COLS - 1], 2 ; [ROWS-1][COLS-1]

Filtro (multiplicación elemento a elemento con el original invertido)

Poner en cero las posiciones donde el original tenía un 1 (rollo de papel). Solo nos importan los espacios vacíos (@) que tienen muchos vecinos.

    xor rcx, rcx
.p1_filter_loop:
    cmp rcx, MATRIX_SIZE
    jge .p1_count

    cmp byte [my_matrix + rcx], 1
    jne .p1_filter_next
    mov byte [result + rcx], 0

.p1_filter_next:
    inc rcx
    jmp .p1_filter_loop

Contar celdas con valor ≥ 5

Note:

  • r12 = contador
.p1_count:
    xor r12, r12
    xor rcx, rcx

.p1_count_loop:
    cmp rcx, MATRIX_SIZE
    jge .p1_done

    cmp byte [result + rcx], 5
    jl .p1_count_next
    inc r12

.p1_count_next:
    inc rcx
    jmp .p1_count_loop

.p1_done:
    mov [counter_p1], r12

Parte 2: loop de propagación

Restaurar la matriz original e iterar hasta saturación. En cada iteración, las celdas con ≥5 vecinos se convierten en rollos de papel.

Restaurar my~matrix~ desde backup

    xor rcx, rcx
.restore_loop:
    cmp rcx, MATRIX_SIZE
    jge .p2_main_loop
    mov al, [backup + rcx]
    mov [my_matrix + rcx], al
    inc rcx
    jmp .restore_loop

    mov qword [counter_p2], 0

.p2_main_loop:

Sumar vecinos (mismo algoritmo que en la parte 1)

    xor r12, r12

.p2_row_loop:
    cmp r12, ROWS
    jge .p2_correction

    xor r13, r13

.p2_col_loop:
    cmp r13, COLS
    jge .p2_next_row

    mov rax, r12
    imul rax, COLS
    add rax, r13
    movzx r14d, byte [my_matrix + rax]

    xor r15, r15

.p2_dir_loop:
    cmp r15, NUM_DIRS
    jge .p2_store_result

    movsx r8, byte [directions + r15*2]
    movsx r9, byte [directions + r15*2 + 1]

    mov r10, r12
    add r10, r8
    mov r11, r13
    add r11, r9

    cmp r10, 0
    jl .p2_next_dir
    cmp r10, ROWS
    jge .p2_next_dir
    cmp r11, 0
    jl .p2_next_dir
    cmp r11, COLS
    jge .p2_next_dir

    mov rax, r10
    imul rax, COLS
    add rax, r11
    movzx eax, byte [my_matrix + rax]
    add r14, rax

.p2_next_dir:
    inc r15
    jmp .p2_dir_loop

.p2_store_result:
    mov rax, r12
    imul rax, COLS
    add rax, r13
    mov byte [result + rax], r14b

    inc r13
    jmp .p2_col_loop

.p2_next_row:
    inc r12
    jmp .p2_row_loop

Corrección (igual que en la parte 1)

.p2_correction:
    xor rcx, rcx
.p2_top_row:
    cmp rcx, COLS
    jge .p2_bottom_row
    add byte [result + rcx], 3
    inc rcx
    jmp .p2_top_row

.p2_bottom_row:
    xor rcx, rcx
.p2_bottom_loop:
    cmp rcx, COLS
    jge .p2_left_col
    mov rax, (ROWS-1) * COLS
    add rax, rcx
    add byte [result + rax], 3
    inc rcx
    jmp .p2_bottom_loop

.p2_left_col:
    mov rcx, 1
.p2_left_loop:
    cmp rcx, ROWS-1
    jge .p2_right_col
    mov rax, rcx
    imul rax, COLS
    add byte [result + rax], 3
    inc rcx
    jmp .p2_left_loop

.p2_right_col:
    mov rcx, 1
.p2_right_loop:
    cmp rcx, ROWS-1
    jge .p2_corners
    mov rax, rcx
    imul rax, COLS
    add rax, COLS-1
    add byte [result + rax], 3
    inc rcx
    jmp .p2_right_loop

.p2_corners:
    add byte [result], 2
    add byte [result + COLS - 1], 2
    add byte [result + (ROWS-1) * COLS], 2
    add byte [result + (ROWS-1) * COLS + COLS - 1], 2

Filtro

    xor rcx, rcx
.p2_filter_loop:
    cmp rcx, MATRIX_SIZE
    jge .p2_count_and_activate

    cmp byte [my_matrix + rcx], 1
    jne .p2_filter_next
    mov byte [result + rcx], 0

.p2_filter_next:
    inc rcx
    jmp .p2_filter_loop

Contar y activar

Contar celdas ≥5 y activarlas en my_matrix para la siguiente iteración.

.p2_count_and_activate:
    xor rbx, rbx            ; rbx = new_activations this iteration
    xor rcx, rcx

.p2_count_loop:
    cmp rcx, MATRIX_SIZE
    jge .p2_check_continue

    cmp byte [result + rcx], 5
    jl .p2_count_next

    inc rbx                         ; new_activations++
    mov byte [my_matrix + rcx], 1   ; activate: becomes paper roll

.p2_count_next:
    inc rcx
    jmp .p2_count_loop

.p2_check_continue:
    ; counter_p2 += new_activations
    add [counter_p2], rbx

    ; if new_activations > 0, continue propagating
    test rbx, rbx
    jnz .p2_main_loop

Salida

Imprimir el resultado de la parte 1

.print_results:
    ; Print "Counter P1: "
    mov rax, 1
    mov rdi, 1
    mov rsi, msg_p1
    mov rdx, msg_p1_len
    syscall

    ; Convert counter_p1 to string
    mov rax, [counter_p1]
    call .print_number_in_rax

Imprimir el resultado de la parte 2

; Print "Counter P2: "
mov rax, 1
mov rdi, 1
mov rsi, msg_p2
mov rdx, msg_p2_len
syscall

; Convert counter_p2 to string
mov rax, [counter_p2]
call .print_number_in_rax

jmp .exit

Subrutina para imprimir números

Convierte el valor en rax a string decimal y lo imprime con newline.

.print_number_in_rax:
    lea rdi, [buffer + 15]
    mov rcx, 10

    test rax, rax
    jnz .convert_loop
    ; Handle zero case
    dec rdi
    mov byte [rdi], '0'
    jmp .do_print

.convert_loop:
    test rax, rax
    jz .do_print
    xor rdx, rdx
    div rcx                 ; rax = quotient, rdx = remainder
    add dl, '0'
    dec rdi
    mov [rdi], dl
    jmp .convert_loop

.do_print:
    ; Calculate length and print
    lea rdx, [buffer + 15]
    sub rdx, rdi            ; rdx = length
    mov rsi, rdi
    mov rax, 1
    mov rdi, 1
    syscall

    ; Print newline
    mov rax, 1
    mov rdi, 1
    mov rsi, newline
    mov rdx, 1
    syscall
    ret

Salida

.exit:
    ;; 下次見~
    mov rax, 60
    xor rdi, rdi
    syscall

Compilar y ejecutar

Compilar main.asm con fasm main.asm y ejecutarlo con ./main. Debería funcionar en todo sistema x86 y x86-64. El input está hardcodeado para estar en ./input.txt

Salida esperada:

Counter P1: <number>
Counter P2: <number>

Algunos benchmarks

Python3

❯ time python3 draft.py Counter P1: 1549 Counter P2: 8887

\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_ Executed in 1.16 secs fish external usr time 1.15 secs 0.00 micros 1.15 secs sys time 0.01 secs 871.00 micros 0.00 secs

PyPy3

❯ time pypy3 draft.py Counter P1: 1549 Counter P2: 8887

\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_ Executed in 215.26 millis fish external usr time 193.14 millis 0.00 millis 193.14 millis sys time 21.98 millis 1.14 millis 20.84 millis

FASM

❯ time ./main #FASM Counter P1: 1549 Counter P2: 8887

\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_ Executed in 29.71 millis fish external usr time 28.89 millis 0.00 micros 28.89 millis sys time 0.77 millis 773.00 micros 0.00 millis

Como era esperable, FASM está en el orden de 10.7x más rápido que el JIT de PyPy, y 58x más rápido que Python. Sé que mi código FASM no es el más eficiente, no está optimizado, y el Python tampoco; este benchmark es solo un dato divertido.

A menos que se indique lo contrario, el contenido del sitio web está bajo la licencia Creative Commons Atribución/Reconocimiento 4.0 Internacional.

© 2026 Germán Caggianese(康青旭)