本記事概要
LLMにお遊びでプログラミングさせてみたので、それを語りたくなっただけです。
一応検証記事ではありますが、実用というより、ネタ記事と思ってください。
やったこと
Github等主要リポジトリで公開されているコードも少ないと思われる下記プログラミング言語について、主要なLLMでコーディングさせてみました。
書かせた言語
- なでしこ
- 日本語プログラミング言語としては本流中の本流
- ただ、日本語プログラミング言語自体が業務領域においてはマイナーです(開発者の方、利用者の方々すみません💦)
- WhiteSpace
- いわゆる難読プログラム言語です
- アセンブラ
- この中では最も主流になりますし、今でも十分使われますが、高級言語ほどのシェアは流石にありません
- x64アーキテクチャ向けのAT&T記法です
※なお私は上記いずれの言語についても詳しくありません。もし本記事中に当該言語についての誤りがあった場合、それは私の無知に起因します。コメント欄でご指摘、ご教示いただけますと幸いです。
使ったLLM
コーディングに用いたLLMは下記の通りです
- ChatGPT 4o, 一部、o3-mini-high
- Gemini 2.0 Flash
- Mercury Coder
- inceptionが2/28に発表した拡散モデルを取り入れたdLLM(diffusion large language model)
- https://www.inceptionlabs.ai/news
- Diffusion EffectをOnにすると、アニメーションが楽しい
ザクっと結果だけ書く
マイナー言語が書けるか
LLM | なでしこ | WhiteSpace |
---|---|---|
ChatGPT | ⚪︎ | ⚪︎ |
Gemini | ⚪︎ | × |
Mercury Coder | × | × |
アセンブラコーディング能力
ビルド | アルゴリズム | 速度 |
---|---|---|
C言語最適化なし | バブルソート | 211.209msec |
C言語O2 | バブルソート | 98.178msec |
アセンブラ | バブルソート | 102.476msec |
C言語標準(qsort) | クイックソート | 1.308msec |
アセンブラ | クイックソート | 1.769msec |
※アセンブラコーディングはChatGPTのみ
なでしこを書かせてみた
書かせたもの
ダイクストラのアルゴリズム
なお私はなでしこの実行環境を持っていないので動作未検証です。
ChatGPT
; ダイクストラのアルゴリズム実装 (隣接行列を使用)
●ダイクストラ(始点, グラフ)とは
各ノード数=グラフの要素数
無限=999999
距離=配列作成(各ノード数, 無限) ; 最短距離を格納する配列
訪問済み=配列作成(各ノード数, 偽) ; ノードの訪問状態
距離[始点]=0
ここから各ノード数回繰り返す
現在ノード=-1
最小距離=無限
; 最小距離のノードを見つける
ここから各ノード数回繰り返す
もし「(訪問済み[対象回数]が偽)かつ(距離[対象回数]<最小距離)」ならば
最小距離=距離[対象回数]
現在ノード=対象回数
ここまで
; もし、全てのノードを訪問済みなら終了
もし「現在ノード=-1」ならば
「全てのノードを訪問しました。」と表示
抜ける
ここまで
; 現在ノードを訪問済みにする
訪問済み[現在ノード]=真
; 隣接ノードを確認し、最短距離を更新
ここから各ノード数回繰り返す
経路長=グラフ[現在ノード][対象回数]
もし「経路長>0」ならば
新距離=距離[現在ノード]+経路長
もし「新距離<距離[対象回数]」ならば
距離[対象回数]=新距離
ここまで
ここまで
ここまで
ここまで
; 最短距離を表示
「最短距離:」と表示
ここから各ノード数回繰り返す
「ノード{対象回数}までの距離は{距離[対象回数]}です。」と表示
ここまで
ここまで
; 使用するグラフ (隣接行列形式)
グラフ=[
[0, 2, 5, 0, 0],
[2, 0, 4, 6, 0],
[5, 4, 0, 2, 8],
[0, 6, 2, 0, 3],
[0, 0, 8, 3, 0]
]
; 始点ノードを設定
始点=0
; アルゴリズムを実行
ダイクストラ(始点, グラフ)
Genini
// ダイクストラ法
ダイクストラ(グラフ, 開始ノード)とは
距離リスト=[:]
確定リスト=[:]
優先度キュー=[開始ノード]
距離リスト[開始ノード]=0
優先度キューが空でない間、繰り返す
現在ノード=優先度キューから最小距離のノードを取得(距離リスト)
確定リスト[現在ノード]=真
もし、グラフ[現在ノード]があれば
グラフ[現在ノード]の各隣接ノードと距離について繰り返す
もし、確定リスト[隣接ノード]が真なら、継続
新しい距離=距離リスト[現在ノード]+距離
もし、距離リスト[隣接ノード]がないか、距離リスト[隣接ノード]>新しい距離なら
距離リスト[隣接ノード]=新しい距離
優先度キューに隣接ノードを追加
終わり
終わり
終わり
終わり
戻る 距離リスト
// 優先度キューから最小距離のノードを取得
優先度キューから最小距離のノードを取得(距離リスト)とは
最小距離=無限大
最小ノード=なし
優先度キューの各ノードについて繰り返す
もし、距離リスト[ノード]があれば
もし、距離リスト[ノード]<最小距離なら
最小距離=距離リスト[ノード]
最小ノード=ノード
終わり
終わり
終わり
もし、最小ノードがあれば
優先度キューから最小ノードを削除
終わり
戻る 最小ノード
// サンプルグラフ
グラフ=[:]
グラフ["A"]=[["B", 2], ["C", 4]]
グラフ["B"]=[["A", 2], ["C", 1], ["D", 5]]
グラフ["C"]=[["A", 4], ["B", 1], ["D", 8], ["E", 10]]
グラフ["D"]=[["B", 5], ["C", 8], ["E", 2], ["F", 6]]
グラフ["E"]=[["C", 10], ["D", 2], ["F", 3]]
グラフ["F"]=[["D", 6], ["E", 3]]
// 開始ノード
開始ノード="A"
// ダイクストラ法を実行
結果=ダイクストラ(グラフ, 開始ノード)
// 結果を表示
結果を「結果:」で表示する
Mercury Coder
import heapq
def dijkstra(graph, start):
# 始点から各ノードへの最短距離を無限大で初期化
distances = {node: float('infinity') for node in graph}
distances[start] = 0
# プライオリティキュー(最小ヒープ)を用意
priority_queue = [(0, start)]
while priority_queue:
# 現在の距離が最も小さいノードを取り出す
current_distance, current_node = heapq.heappop(priority_queue)
# 現在のノードの距離が既に計算された距離よりも大きい場合、スキップ
if current_distance > distances[current_node]:
continue
# 隣接ノードを処理
for neighbor, weight in graph[current_node].items():
distance = current_distance + weight
# 新しい距離が既存の距離よりも短い場合、更新
if distance < distances[neighbor]:
distances[neighbor] = distance
heapq.heappush(priority_queue, (distance, neighbor))
return distances
# グラフの定義
graph = {
'A': {'B': 1, 'C': 4},
'B': {'A': 1, 'C': 2, 'D': 5},
'C': {'A': 4, 'B': 2, 'D': 1},
'D': {'B': 5, 'C': 1}
}
# 始点を指定
start_node = 'A'
# ダイクストラのアルゴリズムを実行
shortest_distances = dijkstra(graph, start_node)
# 結果を表示
print(shortest_distances)
Pythonである…
WhiteSpaceを書かせてみた
書かせたもの
下記のFizzBuzz
- 1-100を数える
- 3の倍数、または3がつく場合は「Three!」と表示
- それ以外の場合は、その数字を表示
なお私はWhiteSpaceの実行環境を持っていないので動作未検証です。
ChatGPT
書けた。
当初は、スペースを[S]、タブを[T]、改行を[L]としたまだしも眼にみえるコードを出してくれたが、散々説得してちゃんとWhiteSpaceにしてもらいました。手こずらせやがって…。
Gemini
書いてくれず。
可読性が低い、実用的でないなど色々言っていました。
Mercury Coder
(Push 1 onto the stack)
\t (Push 100 onto the stack)
\n (Define a label for the loop start)
(Duplicate the top of the stack)
(Push 100 onto the stack)
(Compare the top two elements of the stack)
(If less than, jump to the end of the loop)
(Jump to the label defined earlier)
(Duplicate the top of the stack)
(Convert to string)
(Push "Three!" onto the stack)
(Output the top of the stack as a string)
(Pop the top of the stack)
(Duplicate the top of the stack)
(Push 3 onto the stack)
(Modulo operation)
(Push 0 onto the stack)
(Compare the top two elements of the stack)
(If not equal, jump to the next condition)
(Jump to the label defined earlier)
(Duplicate the top of the stack)
(Convert to string)
(Output the top of the stack as a string)
(Pop the top of the stack)
(Increment the top of the stack)
(Jump back to the loop start)
(End of the program)
書こうとしてくれた努力は見えます。
が、それはWhiteSpaceではない…
アセンブラを書かせてみた
極めた人が書くと超高速だというアセンブラをChatGPTに書かせて、他のビルド方法と比較してみました。
ちょっと苦労話
書かせるのはすんなり行きました。
が、コンパイルが通らない、コンパイルが通っても想定通りに動かない…
ChatGPT 4oに書かせて、Geminiに書かせてうまくいかず、最終的にChatGPT o3-mini highの書いたコードがうまく動きました。
なのでアセンブラの実証は、ChatGPTのみです。
バブルソート
コード
.text
.globl sort_int
.type sort_int, @function
sort_int:
pushq %rbp
movq %rsp, %rbp
pushq %rbx
pushq %r12
pushq %r13
pushq %r14
# r12 = num (配列の要素数)
movq %rsi, %r12
cmpq $1, %r12
jle sort_end # 要素数が1以下なら何もせず終了
xorl %ebx, %ebx # i = 0 を rbx にセット
outer_loop:
# 外側ループ終了条件: if (i >= num - 1) exit
movq %r12, %rax # rax = num
decq %rax # rax = num - 1
cmpq %rax, %rbx # compare i and (num - 1): if (rbx >= rax)
jge sort_end
xorl %r13d, %r13d # j = 0 を r13 にセット
inner_loop:
# 内側ループ終了条件: if (j >= num - i - 1) break
movq %r12, %rax # rax = num
subq %rbx, %rax # rax = num - i
decq %rax # rax = num - i - 1
cmpq %rax, %r13 # compare j and (num - i - 1): if (r13 >= rax)
jge next_outer
# array[j] を %eax、array[j+1] を %edx にロード
movl (%rdi, %r13, 4), %eax # %eax = array[j]
movl 4(%rdi, %r13, 4), %edx # %edx = array[j+1]
cmpl %edx, %eax # if (array[j] <= array[j+1]) は %eax - %edx <= 0
jle no_swap
# swap: array[j] と array[j+1] を交換
movl %edx, (%rdi, %r13, 4)
movl %eax, 4(%rdi, %r13, 4)
no_swap:
incl %r13d # j++
jmp inner_loop
next_outer:
incq %rbx # i++
jmp outer_loop
sort_end:
popq %r14
popq %r13
popq %r12
popq %rbx
popq %rbp
ret
比較対象コード
#include "sort_int.h"
void sort_int(int *array, int num) {
int temp;
// バブルソートの実装
for (int i = 0; i < num - 1; i++) {
for (int j = 0; j < num - i - 1; j++) {
if (array[j] > array[j+1]) {
// 隣接する要素を入れ替え
temp = array[j];
array[j] = array[j+1];
array[j+1] = temp;
}
}
}
}
比較対象のコードもChatGPTに実装させました。
参考:ヘッダ、mainのソース
なお、参考までにヘッダとmain、コンパイルコマンドも書いておきます。
#ifndef SORT_INT_H
#define SORT_INT_H
// 与えられたint配列をバブルソートで並び替える関数の宣言
void sort_int(int *array, int num);
#endif /* SORT_INT_H */
#include <stdio.h>
#include <stdlib.h>
#include <sys/time.h>
#include "sort_int.h" // sort_int の宣言が含まれているヘッダファイル
int main(void) {
FILE *fp = fopen("int.txt", "r");
if (fp == NULL) {
perror("int.txt のオープンに失敗しました");
return EXIT_FAILURE;
}
const int num_count = 10000;
int *array = malloc(sizeof(int) * num_count);
if (array == NULL) {
perror("メモリ確保に失敗しました");
fclose(fp);
return EXIT_FAILURE;
}
// int.txt から 1 万個の整数を読み込む
for (int i = 0; i < num_count; i++) {
if (fscanf(fp, "%d", &array[i]) != 1) {
fprintf(stderr, "インデックス %d での読み込みに失敗しました\n", i);
free(array);
fclose(fp);
return EXIT_FAILURE;
}
}
fclose(fp);
struct timeval start, end;
// ソート前の時刻を取得
if (gettimeofday(&start, NULL) != 0) {
perror("開始時刻の取得に失敗しました");
free(array);
return EXIT_FAILURE;
}
// int 配列をソート (sort_int の実装は外部に依存)
sort_int(array, num_count);
// ソート後の時刻を取得
if (gettimeofday(&end, NULL) != 0) {
perror("終了時刻の取得に失敗しました");
free(array);
return EXIT_FAILURE;
}
// 経過時間をミリ秒単位で計算
long seconds = end.tv_sec - start.tv_sec;
long microseconds = end.tv_usec - start.tv_usec;
double elapsed_ms = seconds * 1000.0 + microseconds / 1000.0;
printf("ソート処理時間: %.3f ミリ秒\n", elapsed_ms);
free(array);
return 0;
}
ご覧の通り、sort_int()の前後で時刻を取り、その差分から処理時間を計測しています。
コンパイルは次のとおりです。
gcc -o sort_c main.c sort_int.c
gcc -o sort_c -O2 main.c sort_int.c
クイックソート
コード
.text
.globl sort_int
.type sort_int, @function
# sort_int(array, num)
# if (num > 1)
# quicksort_internal(array, 0, num-1)
sort_int:
pushq %rbp
movq %rsp, %rbp
cmpq $1, %rsi # if (num <= 1)
jle .Lsort_end
movq %rsi, %rdx # rdx = num
decq %rdx # rdx = num - 1 (high index)
movl $0, %esi # low index = 0
call quicksort_internal
.Lsort_end:
popq %rbp
ret
.globl quicksort_internal
.type quicksort_internal, @function
# quicksort_internal(array, low, high)
# if (low < high) then:
# pivot = partition(array, low, high)
# quicksort_internal(array, low, pivot - 1)
# quicksort_internal(array, pivot + 1, high)
quicksort_internal:
pushq %rbp
movq %rsp, %rbp
pushq %rbx
pushq %r12
pushq %r13
pushq %r14
# rdi: array, rsi: low, rdx: high
movl %esi, %r12d # r12d = low
cmpl %edx, %r12d # if (low >= high) then return
jge .Lqsort_ret
movl %edx, %r14d # r14d = high (保存しておく)
call partition # partition(array, low, high)
# partition の戻り値(pivot のインデックス)が eax に入る
movl %eax, %r13d # r13d = pivot
# 左側の再帰呼び出し: quicksort_internal(array, low, pivot - 1)
# ここで low は r12d に保存済み
movq %rdi, %rdi # array はそのまま
movl %r12d, %esi # low を rsi に
movl %r13d, %eax
subl $1, %eax # pivot - 1
movl %eax, %edx # high = pivot - 1
call quicksort_internal
# 右側の再帰呼び出し: quicksort_internal(array, pivot + 1, original high)
movq %rdi, %rdi # array はそのまま
movl %r13d, %eax
addl $1, %eax # pivot + 1
movl %eax, %esi # low = pivot + 1
movl %r14d, %edx # high = 保存しておいた original high
call quicksort_internal
.Lqsort_ret:
popq %r14
popq %r13
popq %r12
popq %rbx
popq %rbp
ret
.globl partition
.type partition, @function
# partition(array, low, high)
# pivot = array[high]
# i = low - 1
# for (j = low; j < high; j++) {
# if (array[j] <= pivot) { i++; swap(array[i], array[j]); }
# }
# swap(array[i+1], array[high])
# return i+1
partition:
pushq %rbp
movq %rsp, %rbp
pushq %rbx
pushq %r8
pushq %r9
pushq %r10
# rdi: array, rsi: low, rdx: high
# i = low - 1
movl %esi, %r8d # r8d = low
decl %r8d # r8d = low - 1
# pivot = array[high]
movl (%rdi, %rdx, 4), %r10d # r10d = pivot
# j = low
movl %esi, %r9d # r9d = j
.Lpart_loop:
cmpl %edx, %r9d # if (j >= high) → exit loop
jge .Lpart_after_loop
movl (%rdi, %r9, 4), %ebx # ebx = array[j]
cmpl %r10d, %ebx # if (array[j] <= pivot)?
jg .Lpart_skip # if array[j] > pivot, skip
incl %r8d # i++
# swap array[i] and array[j]
movl (%rdi, %r8, 4), %ecx # ecx = array[i]
movl %ebx, (%rdi, %r8, 4) # array[i] = array[j]
movl %ecx, (%rdi, %r9, 4) # array[j] = ecx
.Lpart_skip:
incl %r9d # j++
jmp .Lpart_loop
.Lpart_after_loop:
incl %r8d # i++
# swap array[i] and array[high]
movl (%rdi, %r8, 4), %ecx # ecx = array[i]
movl (%rdi, %rdx, 4), %ebx # ebx = array[high]
movl %ebx, (%rdi, %r8, 4) # array[i] = array[high]
movl %ecx, (%rdi, %rdx, 4) # array[high] = ecx
movl %r8d, %eax # return pivot index = i in eax
popq %r10
popq %r9
popq %r8
popq %rbx
popq %rbp
ret
バブルソートを実装させたチャットから続けて、o3-mini highで実装させたせいか、一発で動きました。
なのでo3-mini highがすごいのかどうかはちょっとわからず…
比較対象コード
#include "sort_int.h"
#include <stdlib.h>
// qsort 用の比較関数(昇順)
static int compare_int(const void *a, const void *b) {
int ia = *(const int *)a;
int ib = *(const int *)b;
if (ia < ib) return -1;
else if (ia > ib) return 1;
else return 0;
}
void sort_int(int *array, int num) {
qsort(array, num, sizeof(int), compare_int);
}
自前で実装させようと思いましたが、C言語のqsort()がほぼクイックソートなのでこれをそのまま使いました。
(厳密にはクイックソートだけではないらしいですね。ソース見たことありませんが…)
ヘッダとmain.cはバブルソートと同じなので省略します。
コンパイルも最適化なしと同じコマンドです。
結果
ビルド | アルゴリズム | 速度 |
---|---|---|
C言語最適化なし | バブルソート | 211.209msec |
C言語O2 | バブルソート | 98.178msec |
アセンブラ | バブルソート | 102.476msec |
C言語標準(qsort) | クイックソート | 1.308msec |
アセンブラ | クイックソート | 1.769msec |
現在のコンパイラって性能高いんですね…
中間コードは吐き出させてないので、ChatGPTに書かせたアセンブラとの比較はしていません(し、アセンブラほとんど読めないのでできません)。
またクイックソートの方も僅差で標準ライブラリの方が早いです。
相当こなれた実装なのだろうなと思います。
考察
ただのネタ記事で終わっても良かったのですが、Qiitaなのである程度この記事を読んでくれた方に有益な示唆を残せるよう考察をします。
なでしこについて
現在の主要なLLMは日本語文献の割合が少ないです。確か1%とかそんなものだったかと思います。
と言っても、全体の量が莫大なため、割合が少なくても高精度に動くそうです。
なでしこは日本語プログラミング言語としては歴史が長く、文献は相応に存在します。
ただ他言語に比べるとソース量はデータセットの中においても非常に少ないあるいは存在しない可能性もあるかと思います。
言語仕様の文献はデータセットに含まれているとすれば、あとは実装例から学習されたのではなく、LLM自身の「推論」で実装されたのでないかと考えています。
Mercury Coderについては、そもそも「なでしこ」というプログラミング言語自体を知らないかのような挙動でした。おそらくそもそもデータセットになかったのでしょう。
WhiteSpace
こちらも同様でおそらくWhiteSpaceの実際の実装例なんて海外のリポジトリ等をみてもほぼないレベルでしょう。
こちらについても言語仕様(と言ってもほとんどメモリ操作のレベル)からすべき操作を「推論」して、実装したものと思われます。
おそらくChatGPTの方がその推論能力において、Geminiより高かったのかと思います。
(Geminiの他のハイエンドなモデルならあるいは実装できたのかもしれません)
Mercury Coderについては、これはあるいは拡散モデルでの実装の弱点なのかもしれません。
チャットを見ると間違いなく存在は知っており、コーディングを試みた形跡もあります。
しかし、出てきたものは、WhiteSpaceの3文字からなるコードではありませんでした。
WhiteSpaceは言語としてみたときに文法と語彙があまりにも異質です。
ChatGPTやGeminiの基礎になっているTransformerが連続的に文を紡ぐのに対し、Diffusionはノイズから全体を成型していきます。
このとき、スペース・タブ・改行の3文字しかないWhiteSpaceだと、命令語としての空白・タブ・改行と、文を構成する文字としての空白・タブ・改行が異なるものとして判断されづらいのかもしれません。
同じようにアセンブラもまた言語としてみたときに、文法・語彙が異質です。
しかし、Mercury Coderでもアセンブラは実装できました(動作未確認)。これはWhiteSpaceに比べ、使用する語彙がはるかに多いこと、学習したソース量が多く文法を推論しやすいからかもしれません。
アセンブラ
どのLLMでも実装できるだろうなと思っていましたが、やはり文法として異質かつソースの学習量からか、他の言語に比べるとうまく動かすのに苦労しました。
しかし、ChatGPTの推論過程を見ると、「きちんと文法」と「語彙の意味」を理解し、「思考」を重ねて実装、ソースを修正していったのがわかりました。
AIにプログラムさせるのは、私も日常的にやっていますが、それでも現在のLLMの推論能力は高いレベルにあるなと感じました。
最後に
実装させたChatGPTのチャットを共有しておきます。
チャットログ(ChatGPT, なでしこ、WhiteSpace)
チャットログ(ChatGPT, アセンブラ)
またこれはおまけでプログラミングするAIが出始めの頃に遊んだときの記事です。
よろしければこちらもご覧ください。