Почему LLM не понимают ассемблер? (И как это исправить)
Спросите у GPT-4o-Mini (актуальная модель на февраль 2026) про оптимизацию циклов в x86-64. Получите красивый, уверенный... бред. Большие языковые модели отлично генерируют Python, JavaScript, даже Rust. Но когда дело доходит до ассемблера, они спотыкаются о простейшие инструкции. Потому что обучали их на высокоуровневых языках, а не на том, как компилятор думает на самом деле.
Вот где появляется LeetCode Assembly Dataset. Не очередной датасет с миллионами строк кода. А 400+ решений задач LeetCode, скомпилированных в четырех архитектурах (x86-64, ARM64, MIPS64, RISC-V) двумя компиляторами (GCC 14.2, Clang 18.1) с разными уровнями оптимизации. Это не просто код. Это отпечаток пальцев компилятора.
Что внутри черного ящика компилятора?
Открываете файл two_sum.c. Пишете наивное решение. Компилируете с -O0, потом с -O3. На выходе - два абсолютно разных ассемблерных кода. Почему? Какие оптимизации сработали? Как регистры распределяются? Куда делись лишние инструкции?
LeetCode Assembly Dataset отвечает на эти вопросы системно. Для каждой задачи из топ-100 LeetCode:
- Исходный код на C (иногда несколько реализаций)
- Ассемблерный вывод для x86-64, ARM64, MIPS64, RISC-V
- Сборки через GCC 14.2 и Clang 18.1 (последние стабильные версии на 2026 год)
- Уровни оптимизации от -O0 (дебаг) до -O3 (агрессивная оптимизация)
- Сравнительные метрики: размер кода, количество инструкций
| Архитектура | Компилятор | Оптимизации | Пример отличий |
|---|---|---|---|
| x86-64 | GCC 14.2 | -O0, -O1, -O2, -O3, -Os | SIMD инструкции при -O3 |
| ARM64 | Clang 18.1 | -O0, -O1, -O2, -O3, -Oz | Другое распределение регистров |
| RISC-V | GCC 14.2 | -O0 до -O3 | Минимальный набор инструкций |
Зачем это вашей LLM? (Неочевидные применения)
Кажется, что обучать LLM на ассемблере - это как учить поэта бухгалтерскому учету. Но посмотрите глубже:
1. Понимание оптимизаций компилятора
Модель видит не просто "вот ассемблерный код". Она видит трансформацию: как цикл развернулся, как переменная инлайнилась, как мертвый код исчез. После обучения на таких примерах, LLM начинает предсказывать, какие оптимизации компилятор применит к конкретному коду. Полезно для код-ревью с LLM - модель может находить места, где компилятор не сможет оптимизировать.
2. Кросс-архитектурная переносимость
Одна и та же функция на x86-64 и ARM64 выглядит по-разному. Но семантика одинакова. Модель учится абстрагироваться от конкретных инструкций и понимать намерение кода. Это как переводчик, который понимает смысл, а не просто подбирает слова. Пригодится, если вы fine-tune модель под новый язык программирования - принципы те же.
3. Генерация оптимизированного кода
Вместо того чтобы генерировать наивный C-код, LLM может сразу предлагать реализации, которые хорошо оптимизируются. Знает, что компилятор любит маленькие функции (инлайнинг), предпочитает циклы с предсказуемыми условиями, ненавидит указательные алиасы.
Важный нюанс: не путайте этот датасет с обычными парсированными примерами ассемблера. Здесь каждая пара "C-код → ассемблер" гарантированно корректна и получена через реальную компиляцию. Нет случайных ошибок, нет некорректных примеров.
Как загрузить и подготовить данные (без головной боли)
Первая проблема: датасет весит под 50 ГБ в сыром виде. Вторая проблема: структура неочевидна. Третья: как превратить это в формат для обучения?
1 Скачиваем только нужное
Не качайте все сразу. Начните с одной архитектуры. Например, x86-64 наиболее распространена и дает хорошую базу. Используйте выборочное скачивание:
# Скачиваем только x86-64 с GCC
wget -r -np -R "index.html*" --accept "*x86_64*gcc*" https://dataset.url/leetcode-asm/
# Или используем streaming подход как в
# Datasets streaming=True
# чтобы не хранить всё локально
2 Парсим структуру
Файлы организованы по шаблону: {problem}/{compiler}/{arch}/{optimization}.asm. Нужно собрать это в таблицу соответствий:
import json
from pathlib import Path
dataset_path = Path("leetcode-asm")
records = []
for problem_dir in dataset_path.iterdir():
if not problem_dir.is_dir():
continue
# Читаем исходный C-код
c_file = problem_dir / "solution.c"
if not c_file.exists():
continue
c_code = c_file.read_text()
# Собираем все ассемблерные варианты
for asm_file in problem_dir.glob("**/*.asm"):
# Извлекаем метаданные из пути
# например: two_sum/gcc/x86_64/O3.asm
parts = asm_file.relative_to(problem_dir).parts
record = {
"problem": problem_dir.name,
"c_code": c_code,
"asm_code": asm_file.read_text(),
"compiler": parts[0], # gcc или clang
"arch": parts[1], # x86_64, arm64 и т.д.
"optimization": parts[2].replace('.asm', ''), # O0, O1, O2, O3
"file_path": str(asm_file)
}
records.append(record)
# Сохраняем структурированный датасет
with open("leetcode_asm_structured.jsonl", "w") as f:
for record in records:
f.write(json.dumps(record) + "\n")
3 Создаем пары для обучения
Для обучения нужны не просто файлы, а осмысленные пары. Есть несколько стратегий:
- Прямой перевод: C-код → ассемблер (самое простое)
- Оптимизационные пары: C-код с -O0 и тот же код с -O3 (учит оптимизации)
- Кросс-компиляторные пары: GCC output → Clang output для одного кода (учит различиям компиляторов)
- Кросс-архитектурные пары: x86-64 → ARM64 (учит переносимости)
Совет: Начните с прямого перевода. Когда модель освоит базовый синтаксис, добавьте оптимизационные пары. Такой прогрессивный подход дает лучшие результаты, чем обучение всему сразу.
Форматирование промптов: как разговаривать с LLM об ассемблере
Самая большая ошибка - просто скормить модели сырой ассемблер. Нужна структура. Нужны аннотации. Нужен контекст.
Плохой промпт (так не делайте):
Вот ассемблерный код:
mov eax, [rbp-4]
add eax, 1
mov [rbp-4], eax
Что он делает?
Хороший промпт (так и делайте):
Архитектура: x86-64
Компилятор: GCC 14.2 с оптимизацией -O1
Исходный C-код: counter++;
Ассемблерный вывод с комментариями:
# Загружаем значение переменной counter из стека
mov eax, DWORD PTR [rbp-4]
# Увеличиваем на 1
add eax, 1
# Сохраняем обратно в counter
mov DWORD PTR [rbp-4], eax
Вопрос: Почему используется стек (rbp-4), а не регистр?
Разница очевидна. Во втором случае модель получает:
- Архитектурный контекст
- Информацию о компиляторе и оптимизациях
- Исходный высокоуровневый код
- Аннотированный ассемблер
- Конкретный вопрос на понимание
При обучении используйте такой же структурированный формат. Создайте шаблон:
def create_training_example(c_code, asm_code, compiler, arch, optimization):
prompt = f"""Архитектура: {arch}
Компилятор: {compiler} с оптимизацией {optimization}
Исходный C-код:
{c_code}
Сгенерируй ассемблерный код для этой архитектуры:
"""
completion = f"""{asm_code}"""
return {"prompt": prompt, "completion": completion}
Обучение модели: от CodeLlama к специализированному ассемблерному эксперту
Брать голую GPT-4 и fine-tune на ассемблере - дорого и неэффективно. Лучше начать с модели, которая уже понимает программирование.
Выбор базовой модели (2026 год):
| Модель | Плюсы для ассемблера | Минусы |
|---|---|---|
| CodeLlama-34B-Instruct | Уже знает синтаксис, понимает контекст кода | Большая, требует много памяти |
| DeepSeek-Coder-33B | Отличное понимание алгоритмов, мультиязычность | Меньше опыта с низкоуровневым кодом |
| Phi-3.5-Code (28B) | Эффективная, хорошо обучается на маленьких датасетах | Может потребоваться больше эпох |
Мой выбор на 2026: CodeLlama-34B-Instruct. Она уже обучена на множестве языков программирования, включая немного ассемблера. Fine-tune пойдет быстрее.
Процесс обучения:
# Подготовка датасета в формате для Hugging Face
python prepare_dataset.py \
--input leetcode_asm_structured.jsonl \
--output hf_dataset \
--format chatml # или alpaca, в зависимости от модели
# Запуск обучения с PEFT/LoRA (экономия памяти)
accelerate launch train_lora.py \
--model_name "codellama/CodeLlama-34b-Instruct-hf" \
--dataset_path "hf_dataset" \
--output_dir "leetcode_asm_expert" \
--lora_r 16 \
--lora_alpha 32 \
--num_train_epochs 3 \
--per_device_train_batch_size 1 \
--gradient_accumulation_steps 8
Ошибки, которые сломают ваше обучение (проверено на горьком опыте)
Ошибка 1: Смешивание архитектур в одной эпохе. Модель путает x86-64 с ARM64. Решение: обучайте отдельно на каждой архитектуре, потом объединяйте.
Ошибка 2: Игнорирование уровней оптимизации. Модель не понимает разницу между -O0 и -O3. Решение: явно указывайте уровень оптимизации в промпте и включайте примеры всех уровней.
Ошибка 3: Обучение на сыром ассемблере без комментариев. Модель запоминает инструкции, но не понимает семантику. Решение: добавляйте автоматические комментарии (можно сгенерировать простым парсером).
Ошибка 4: Слишком большие контексты. Ассемблерные файлы бывают огромными. Решение: разбивайте на функции, обучайте на отдельных процедурах. Или используйте методы вроде RLM для управления контекстом.
Что можно сделать с обученной моделью? (Не только генерировать ассемблер)
Генерация кода - это очевидно. Но есть более интересные применения:
1. Анализ оптимизаций компилятора
Дайте модели два варианта ассемблера (с -O0 и -O3). Спросите: "Какие оптимизации применил компилятор?". Модель будет находить loop unrolling, constant propagation, dead code elimination. Полезно для обучения junior-разработчиков.
2. Кросс-компиляционный перевод
"Вот ассемблер от GCC для x86-64. Как будет выглядеть аналогичный код от Clang для ARM64?". Модель учится абстрагироваться от конкретного синтаксиса и понимать семантику операций.
3. Поиск оптимизационных возможностей
"Вот этот C-код. Где компилятор не сможет оптимизировать из-за pointer aliasing?". Модель указывает на проблемные места, предлагает альтернативы. Как персональный компиляторный эксперт.
4. Образовательный инструмент
Студенты пишут C-код, модель показывает, как он будет выглядеть на ассемблере для разных архитектур. С объяснениями, почему выбраны те или иные инструкции. Лучше любой книги по компиляторам.
Интеграция с существующими пайплайнами
Обученная модель - не изолированный инструмент. Вот как встроить ее в ваш workflow:
# Пример: интеграция в CI/CD для проверки оптимизаций
import openai
def check_compiler_optimizations(c_code, target_arch="x86_64"):
"""Проверяет, хорошо ли оптимизируется код"""
prompt = f"""Архитектура: {target_arch}
Компилятор: GCC 14.2 с -O3
Исходный код:
{c_code}
Какие оптимизации компилятор сможет применить к этому коду?
Укажи конкретные техники и их ожидаемый эффект."""
response = openai.ChatCompletion.create(
model="your-finetuned-model",
messages=[{"role": "user", "content": prompt}]
)
return response.choices[0].message.content
# Использование в ревью кода
if __name__ == "__main__":
problematic_code = """
int sum = 0;
for (int i = 0; i < 1000; i++) {
sum += array[i];
}
"""
analysis = check_compiler_optimizations(problematic_code)
print(f"Оптимизационный анализ:\n{analysis}")
Что дальше? (Когда ассемблера недостаточно)
LeetCode Assembly Dataset - отличный старт. Но это только первый шаг. Что делать, когда модель освоила ассемблер?
Добавьте реальный мир: Возьмите принципы из билингвального обучения и добавьте парные примеры из реальных проектов (Linux kernel, Redis, nginx).
Углубитесь в оптимизации: Добавьте информацию о микроархитектуре (pipelining, cache lines, branch prediction). Модель должна понимать не только ЧТО делает инструкция, но СКОЛЬКО она стоит.
Экспериментируйте с архитектурой: Может быть, токены - не лучший способ представлять ассемблер? Посмотрите на альтернативные подходы с латентным пространством.
Создайте синтетические данные: Когда закончатся реальные примеры, генерируйте свои с помощью уже обученной модели. Как в техниках создания синтетических данных, но для ассемблера.
Финал: компилятор как язык, ассемблер как диалект
LeetCode Assembly Dataset - это не просто коллекция файлов. Это Rosetta Stone между высокоуровневым мышлением программиста и низкоуровневой логикой процессора.
Обучая LLM на этом датасете, вы не просто создаете инструмент для генерации ассемблера. Вы создаете переводчика между человеком и машиной. Модель, которая понимает, КАК компилятор думает. КАК процессор выполняет. КАК оптимизации трансформируют код.
В 2026 году, когда все говорят о мультимодальных моделях и AGI, такой узкоспециализированный навык кажется анахронизмом. Но именно такие анахронизмы - понимание фундаментальных основ - делают разницу между моделью, которая умеет болтать, и моделью, которая умеет думать.
Начните с x86-64 и GCC. Добавьте комментарии к инструкциям. Обучите на парах с разными оптимизациями. И через несколько эпох ваша LLM начнет понимать то, что раньше было магией: как i++ превращается в add eax, 1, а потом - в одну инструкцию inc, а потом - вообще исчезает в оптимизированном цикле.
Это и есть настоящая магия компиляторов. И теперь ваша модель знает секрет.