Перейти к содержанию
Публикация AiManual

LeetCode Assembly Dataset: когда компилятор говорит с ИИ на ассемблере

Полный гайд по LeetCode Assembly Dataset: как обучать LLM на 400+ решениях x86-64/ARM64/MIPS64/RISC-V с GCC/Clang оптимизациями для низкоуровневого программиров

Коротко

Что будет в материале

  1. 01

    Почему LLM не понимают ассемблер? (И как это исправить)

  2. 02

    Что внутри черного ящика компилятора?

  3. 03

    Зачем это вашей LLM? (Неочевидные применения)

  4. 04

    Как загрузить и подготовить данные (без головной боли)

Почему 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), а не регистр?

Разница очевидна. Во втором случае модель получает:

  1. Архитектурный контекст
  2. Информацию о компиляторе и оптимизациях
  3. Исходный высокоуровневый код
  4. Аннотированный ассемблер
  5. Конкретный вопрос на понимание

При обучении используйте такой же структурированный формат. Создайте шаблон:

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
💡
Используйте LoRA (Low-Rank Adaptation) вместо полного fine-tune. Для ассемблера достаточно обучить 1-2% параметров модели. Экономит время, память, и модель не "забывает" другие языки программирования. Подробнее о масштабировании тонкой настройки в нашем руководстве по SageMaker.

Ошибки, которые сломают ваше обучение (проверено на горьком опыте)

Ошибка 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, а потом - вообще исчезает в оптимизированном цикле.

Это и есть настоящая магия компиляторов. И теперь ваша модель знает секрет.