---
title: "vector-length agnostic: 열 개를 5개씩 나누어도 빠뜨리지 않는 루프"
author: "브레이브브라우저쓰자"
published_at: "2026-10-16T23:04:02.117Z"
updated_at: "2026-10-06T00:47:50.481Z"
tags: ["vector-length agnostic"]
canonical_url: "https://ariser.io/@writer_a9817d5a8de0/vector-length-agnostic-%EC%97%B4-%EA%B0%9C%EB%A5%BC-5%EA%B0%9C%EC%94%A9-%EB%82%98%EB%88%84%EC%96%B4%EB%8F%84-%EB%B9%A0%EB%9C%A8%EB%A6%AC%EC%A7%80-%EC%95%8A%EB%8A%94-%EB%A3%A8%ED%94%84"
ai_assisted: true
---

# vector-length agnostic: 열 개를 5개씩 나누어도 빠뜨리지 않는 루프

이 글은 AI가 활용됐습니다.

[원문](https://ariser.io/@writer_a9817d5a8de0/vector-length-agnostic-%EC%97%B4-%EA%B0%9C%EB%A5%BC-5%EA%B0%9C%EC%94%A9-%EB%82%98%EB%88%84%EC%96%B4%EB%8F%84-%EB%B9%A0%EB%9C%A8%EB%A6%AC%EC%A7%80-%EC%95%8A%EB%8A%94-%EB%A3%A8%ED%94%84)

32비트 정수 10개에 각각 1을 더하는 벡터 루프를 만들었다. 한 코어는 벡터 레지스터 한 개에 4개를 담고, 다른 코어는 8개를 담는다. 코드를 두 벌 만들지 않고 모두 처리하려면, 루프가 “이번에는 몇 개를 처리했는가”를 하드웨어에서 받아야 한다. 이것이 vector-length agnostic 코드의 출발점이다.

## 최대 용량과 이번 작업량은 다르다

RISC-V 벡터 명령에서 VLEN은 벡터 레지스터 한 개의 비트 길이, SEW는 설정한 원소 폭이다. LMUL=1로 한 레지스터를 사용하면 SEW=32에서 VLEN=128인 구현의 VLMAX는 4, VLEN=256인 구현에서는 8이다. 여기서 VLMAX는 설정상 한 번에 다룰 수 있는 최대 원소 수이고, 실제 작업량은 vl이다. [RISC-V Vector, Vector Register Grouping 및 Vector Length Register](https://github.com/riscv/riscv-isa-manual/blob/main/src/unpriv/vector-common.adoc)

프로그램은 vsetvli에 남은 원소 수를 AVL로 전달한다. 명령은 해당 설정에서 사용할 vl을 정하고, 목적지 정수 레지스터에도 그 값을 돌려준다. 프로그램이 처리 개수를 고정해 추측하지 않고 이 반환값을 사용하는 것이 핵심이다.

예제는 두 구현이 e32,m1과 아래 명령을 지원하며, 입력·출력 배열의 주소가 유효하고 두 배열이 겹치지 않는다고 가정한다. a0에는 남은 원소 수, a1과 a2에는 각각 입력·출력 주소가 들어 있다.

```asm
    beqz a0, done
loop:
    vsetvli t0, a0, e32, m1, ta, ma
    vle32.v v0, (a1)
    vadd.vi v0, v0, 1
    vse32.v v0, (a2)
    slli t1, t0, 2
    add a1, a1, t1
    add a2, a2, t1
    sub a0, a0, t0
    bnez a0, loop
done:
```

t0가 이번 vl이다. 32비트 정수는 4바이트이므로 주소는 4×t0만큼 전진하고, 남은 수에서는 t0를 뺀다. 덧셈은 각 활성 원소에 독립적으로 적용된다. mask를 쓰지 않으므로 v0를 데이터 레지스터로 사용한다. `ta,ma` 설정에서 비활성 영역이 어떤 유효 결과를 가진다고 기대하지 않으며, 저장도 이번 활성 원소만 대상으로 한다.

## 4개씩과 8개씩은 가능한 실행 예다

10개를 VLMAX=4인 구현이 4,4,2개로 처리한다고 하자. 첫 반복은 인덱스 0~3을 쓰고 16바이트 전진한다. 두 번째는 4~7을 쓰고 다시 16바이트 전진하며, 마지막은 8~9를 쓰고 8바이트 전진한다. 원소마다 정확히 한 번 1을 더한다.

VLMAX=8인 구현이 8,2개로 처리하면 첫 반복에서 0~7, 다음에서 8~9를 다룬다. 반복 횟수는 달라도 같은 입력 배열에 대한 결과는 같다. 두 추적은 규약이 허용하는 예이며, 모든 구현이 반드시 이 묶음을 택한다는 뜻은 아니다.

여기서 흔한 함정이 있다. vl이 항상 min(AVL,VLMAX)라고 단정하는 것이다. 원문은 AVL이 VLMAX보다 크고 2×VLMAX보다 작을 때, ceil(AVL/2)부터 VLMAX까지의 값을 허용한다. 남은 두 반복의 작업을 더 고르게 나누기 위해서다. [RISC-V Vector, Constraints on Setting vl](https://github.com/riscv/riscv-isa-manual/blob/main/src/unpriv/vector-common.adoc)

따라서 AVL=10, VLMAX=8에서는 첫 vl이 5인 구현도 가능하다. 그 경우 추적은 5,5가 된다. 위 코드는 t0=5를 받아 주소를 20바이트 전진시키고 남은 수를 5로 바꾸므로 정상 동작한다. 첫 반복에서 8개를 처리했다고 가정해 주소를 32바이트 전진시키면, 실제로는 인덱스 0~4만 처리한 뒤 5~7을 건너뛰게 된다.

## 반환값 하나가 경계를 책임진다

vl 설정 규약에는 종료를 뒷받침하는 조건도 있다. AVL이 VLMAX 이하이면 vl은 AVL과 같고, 양의 AVL에서는 양의 vl이면서 AVL을 넘지 않는다. AVL이 2×VLMAX 이상이면 vl은 VLMAX다. 따라서 지원되는 설정에서 반환된 vl로 남은 수를 줄이면 마지막 조각까지 처리할 수 있다.

이 방식은 큰 배열을 하드웨어 용량에 맞는 조각으로 나누는 strip mining이다. 별도로 마지막 2개만 처리하는 scalar 루프를 반드시 만들 필요가 없고, 하드웨어 용량이 달라져도 같은 구조를 쓸 수 있다. RISC-V 원문의 예제 역시 반환된 vl에 원소 크기를 곱해 주소를 갱신한다. [RISC-V Vector, Example of strip mining and changes to SEW](https://github.com/riscv/riscv-isa-manual/blob/main/src/unpriv/vector-common.adoc)

물론 결과 호환성이 성능까지 같게 만들지는 않는다. 더 긴 레지스터를 가진 코어가 실제로 더 많은 원소를 한 사이클에 계산하는지는 실행 lane, 메모리 대역폭과 명령 지연에 달렸다. 예제처럼 독립적인 원소 덧셈 대신 부동소수점 합산 순서를 바꾸는 알고리즘이라면 수치 결과의 별도 조건도 검토해야 한다.

vector-length agnostic 루프는 하드웨어 길이를 모른 채 무작정 실행하는 루프가 아니다. 매 반복마다 지원되는 설정에서 실제 작업량을 받고, 그 수만큼 계산·주소·잔여량을 함께 갱신한다. 10개를 4,4,2로 나누든 5,5로 나누든 빠뜨리지 않는 이유는 넉넉한 최대 용량을 믿어서가 아니라, 반환된 vl을 진행의 기준으로 삼기 때문이다.

