컴퓨터가 할 수 있는 것은 베껴 쓰기(로드 혹은 세이브), 연산(사칙연산 및 논리연산), 그리고 분기(건너뛰기)뿐이다. 따라서 이들만 마인크래프트 바닐라 명령어로 구현할 수 있다면 원리적으로는 프로그래밍이 가능하다.
마인크래프트 명령어와 어셈블리어는 다음과 같은 이유에서 굉장히 유사하다.
1. 연산에 직접 사용할 수 있는 메모리가 제한적이다. 그 외의 메모리에 있는 자료를 사용하려면 먼저 연산이 가능한 메모리에 베껴 써야 한다. CPU에서는 레지스터, 마크에서는 스코어보드가 여기에 해당한다.
2. 연산 명령의 형식이 고정적이다. 즉, 1줄의 명령만으로는 혼합 계산이 불가능하다. 이 때문에 CPU의 레지스터 중에는 임시값을 넣어두는 버퍼 용도의 레지스터가 있다.
3. 연산 대상이 변수인 경우(이하 R유형)와 상수인 경우(이하 I유형)를 구분하며, 곱셈과 같이 '무거운' 연산은 변수끼리만 가능하다. (비트 수를 N이라 할 때 덧셈, 뺄셈의 시간복잡도는 O(N)인데 반해 곱셈, 나눗셈은 O(N^2)이다. 그래서 실제 CPU에는 ALU 외에 이런 무거운 연산을 고속으로 처리하는 비트 컨볼버가 따로 있다.)
4. 모든 메모리는 전역적이다. 이 특징은 다음의 게시글을 참고하라.
20.09.5.23 무현이스킨을 않끼면 벤입니다
gall.dcinside.com
이 외에도 형식적으로도 스코어보드 전용 가상 엔티티가 $로 시작하는 점도 유사하다.
주 차이점은 다음과 같다. 물론 어셈블리어는 기기 사양에 따라 다를 수 있지만 여기서는 MIPS 기준으로 설명한다.
* 마크 커맨드의 강점
1. 스토리지에서 다양한 자료 구조를 사용할 수 있다. 구체적으로는 배열과 해시(파이썬 유저는 이 자료구조를 딕셔너리라고 부른다)가 제공된다. 배열의 차원에는 제한이 없으며, 해는 구조체처럼 사용할 수도 있다. 이에 반해 어셈블리어의 데이터 메모리는 1차원 배열로만 사용할 수 있다.
2. 틱 레이트가 CPU의 클럭 레이트에 비해 훨씬 느리기 때문에 다양한 단일(Atommical) 명령을 사용할 수 있다. 반면 RISC 구조인 MIPS에서는 단일 명령이 운영체제의 스핀락과 같은 특수 목적으로만 사용된다.
3. 조건문을 중첩해서 달 수 있다.
* 어셈블리의 강점
1. 비트별 논리 연산(and, xor, lsh 등)이 지원된다. 다음의 기수 정렬 코드 일부를 비교하면서 논리 연산이 있고 없고의 차이를 알아보자. MIPS Assembly에서는 비트를 우측으로 2칸씩 밀어 4로 나눈 몫을, 비트 마스크 3을 and 연산하여 나머지를 찾았다. 그러나 mcfunction 코드에서는 동일한 기능을 위해 $mul = 4를 선언하였다.
MIPS Assembly
...
# Shamt Set
li $s3, 0
...
# Append All Number into the Additionals
mov $t0, $s0
PUSH:
lw $t1, 0($t0)
srlv $t2, $t1, $s3 # Radix Shifting
andi $t2, $t2, 3 # Modulo for 4 (Bit Masking) => Radix on Base 4
...
addi $s3, $s3, 2
...
Minecraft Function
...
scoreboard players set $s2 value 256
scoreboard players set $s3 value 1
scoreboard players set $mul value 4
...
execute store result storage sorting:arguments a0 int 1 run scoreboard players get $t0 value
function sort:transfer/lmain with storage sorting:arguments
scoreboard players operation $t2 value = $v0 value
scoreboard players remove $t2 value 1
scoreboard players operation $t2 value /= $s3 value
scoreboard players operation $t2 value %= $mul value
...
# Escape
setblock 4 0 14 air
scoreboard players operation $s3 value *= $mul value
...
2. 쓰기 레지스터를 자유롭게 쓸 수 있다. 마인크래프트 스코어보드 연산자는 기본적으로 Accumulator 방식이라서 쓰기 스코어보드가 첫 번째 읽기 스코어보드로 고정된다.
3. 분기 문법(beq, j 등)이 비교적 자유롭다. 마인크래프트 데이터팩 함수의 유일한 분기 문법은 함수 호출뿐이다. 이 약점은 특히 반복문에서 두드러지는데, 반복을 위해 재귀함수를 쓸 수밖에 없기 때문이다.
어셈블리어의 마인크래프트 구현:
scoreboard 명령어에서 R유형의 키워드는 operation이다. I유형은 add, remove, set의 3가지 키워드가 있는데, add 외에 remove(뺄셈)가 따로 있는 것은 이 둘은 음수를 쓰지 못하기 때문이다. (어셈블리어에는 subi 명령어를 별도로 두지 않는다.)
마크에서는 execute store result를 사용하면 스코어보드와 스토리지 사이에서 자료를 전달할 수 있다.
로드의 예:
# Minecraft Function
execute store result score $t0 value run data get storage minecraft:arm/bram s0[3]
# Equivalent MIPS Assembly Code
lw $t0, 12($s0)
세이브의 예:
# Minecraft Function
execute store result storage minecraft:arm/bram s0[3] int 1 run scoreboard players get $t0 value
# Equivalent MIPS Assembly Code
sw $t0, 12($s0)
위의 예시에 주어진 어셈블리어의 lw, sw는 I유형 명령어의 일종이라서, 이 예문처럼 상수로만 인덱싱할 수 있다. 만일 s0[i]처럼 변수로 인덱싱을 하고 싶다면, 다음과 같이 직접 덧셈해서 그 주소값을 구해야 한다.
sll $t0, $s1, 2 # $t0 = 4 * i
add $t1, $s0, $t0 # $t1 = $s0 + 4i = &s0[i]
lw $t2, 0($t1) # $t2 = $t1[0] = s0[i]
마크에서는 스토리지의 이름을 커맨드에 완벽히 작성해야만 접근이 가능하기 때문에 마찬가지로 상수 인덱싱만이 가능하고, 변수 인덱싱을 위해서는 매크로 함수가 필요하다. 따라서 정렬 알고리즘이나 신호 처리 등 변수 인덱스를 쓸 일이 많은 프로그램을 작성 중이라면 먼저 load, save 매크로 함수 및 인덱스 전달 해시 정도는 만들어 두는 것을 권장한다.
분기 문법의 마인크래프트 구현은 다음 게시글에서 소개한다.
20.09.5.23 무현이스킨을 않끼면 벤입니다
gall.dcinside.com
참고로 예문으로 제시한 기수 정렬 프로그램은 데이터팩 정렬 맵에 실제로 쓰였던 코드임