Как написать расширение SQLite на C и Rust с помощью Claude: разбор эксперимента Саймона Уиллисона

Как написать расширение SQLite на C и Rust с помощью Claude: разбор эксперимента Саймона Уиллисона

По запросу автора Claude Opus 5.5 написал четыре версии расширения parse_number(), где оптимизированный код на C обогнал Rust с результатом 16–19 наносекунд на строку.

Задача: парсинг чисел из неструктурированных строк внутри базы

8 ноября 2026 года соавтор Django и создатель Datasette Саймон Уиллисон опубликовал технический разбор эксперимента с Claude Opus 5.5. Задача заключалась в создании скалярной функции parse_number(value) для SQLite. Функция должна на лету очищать строковые значения от знаков валют, разделителей тысяч и процентов, корректно обрабатывать отрицательные числа и возвращать NULL для некорректных строк.

SELECT parse_number('-$1,234.56'); -- -1234.56 (REAL) SELECT parse_number('£1,000,000'); -- 1000000 (INTEGER) SELECT parse_number('12.5%'); -- 12.5 (REAL) SELECT parse_number('n/a'); -- NULL

Уиллисон сформулировал точные правила парсинга:

  • Пробелы, табуляции и переводы строк по краям отбрасываются, а символы ,, $, £ и % удаляются из любой позиции строки.
  • Допустим один знак минус перед первой цифрой или точкой, поэтому конструкции -$5 и $-5 обрабатываются одинаково.
  • Числа без точки возвращаются как 64-битный INTEGER, а при переполнении преобразуются в REAL.
  • Строки с десятичной точкой возвращаются как REAL.
  • Экспоненциальная запись 1e5, лишние знаки плюс +5 и пробелы внутри числа переводят результат в NULL.

Саймон открыл диалог с Claude Opus 5.5 и попросил сравнить реализацию расширения на C и Rust по производительности, сложности кода и размеру итогового файла .so.

Промпты к Claude Opus 5.5 и первые результаты замеров. Источник: simonwillison.net
Промпты к Claude Opus 5.5 и первые результаты замеров. Источник: simonwillison.net

Первый раунд: почему базовый C проиграл Rust

Claude написал три начальных варианта расширения:

  • Базовый C (75 строк кода) с компиляцией в компактную библиотеку размером 14 КБ, где десятичные дроби отправлялись в стандартную функцию strtod().
  • Rust на библиотеке rusqlite 0.41 (36 строк плюс 41 строка общего парсера) размером 321 КБ.
  • Rust на библиотеке sqlite-loadable 0.0.6-alpha.6 (30 строк плюс 41 строка общего парсера) размером 298 КБ.

Для тестирования Уиллисон загрузил расширения в Python-модуль sqlite3 (SQLite 3.53.1) на виртуальной машине Linux x86-64 с 2 vCPU под управлением GCC 13.3 и Rustc 1.97. Замер проводился через SELECT sum(parse_number(v)) FROM t на выборке из 2 миллионов строк с вычитанием базового времени сканирования sum(length(v)).

Результаты первого раунда на смешанной выборке распределились так:

  • Чистый C тратил 67,3 наносекунды на строку.
  • Версия на rusqlite выполнялась за 45,9 наносекунды.
  • Версия на sqlite-loadable показала 40,5 наносекунды.

Разделение выборки объяснило причину. На целых числах C оказался быстрее (25,1 наносекунды против 32–36 наносекунд у Rust). Однако на десятичных дробях C показал 106,3 наносекунды против 46,5–54,5 наносекунды у Rust. Причиной отставания C стала стандартная функция strtod() из библиотеки glibc: она универсальна, зависит от текущей системной локали и выполняет проверки, замедляющие обработку. Парсер Rust из стандартной библиотеки использует оптимизированный быстрый путь и алгоритм Eisel-Lemire.

Второй раунд: Clinger's fast path и разгон C до 16 наносекунд

Уиллисон отправил модели следующий промпт: написать собственную функцию разбора десятичных дробей на C, которая догонит или опередит реализацию на Rust.

Claude переписал парсер C, внедрив быстрый путь Клингера (Clinger's fast path). Алгоритм собирает первые 19 значащих цифр в 64-битное целое число mant и считает количество знаков после запятой frac. Если мантисса укладывается в 2^53 (53 бита формата IEEE 754), а количество дробных знаков не превышает 22, значение вычисляется одной операцией деления с аппаратным округлением:

if (FAST_PATH_OK && nsig <= 19 && mant <= ((uint64_t)1 << 53) && frac <= 22) { double v = (double)mant / POW10[frac]; sqlite3_result_double(ctx, neg ? -v : v); return; } parse_slow(ctx, s, start, end);

После добавления 30 строк оптимизации итоговая версия c-fast обошла конкурентов на бенчмарке в 2 миллиона строк:

  • На смешанной выборке время составило 19,2 наносекунды на строку против 40,5–45,9 наносекунды у Rust.
  • На целых числах время составило 15,8 наносекунды против 32,1–36,4 наносекунды у Rust.
  • На десятичных дробях время составило 17,5 наносекунды против 46,5–54,5 наносекунды у Rust и 106,3 наносекунды у базового C.
  • Встроенная конструкция на базе SQL-функции replace() показала 328–357 наносекунд на строку, уступив C-расширению в 17–20 раз.
Сравнение производительности и размера бинарников. Источник: simonw/research
Сравнение производительности и размера бинарников. Источник: simonw/research

Надёжность и верификация: 8 миллионов проверок фаззингом

Для проверки корректности кода Уиллисон попросил Claude оформить исследование в виде готового репозитория simonw/research. Модель сгенерировала структуру с исходным кодом, скриптами сборки и тестами.

Проверка включала два этапа тестирования:

  • 40 ручных тест-кейсов в test.py, проверяющих граничные значения и функцию typeof() для всех четырёх версий расширения.
  • Фаззинг-тест fuzz.py, сравнивающий побитовое совпадение результатов с эталонной реализацией float() в Python.
  • 8 миллионов проверок (4 сида по 500 тысяч случайных строк для каждого расширения), в которых генератор передавал случайный шум, дроби с числом знаков от 1 до 25 и точные числа вблизи границы 2^53.

Все четыре варианта расширения прошли 8 миллионов проверок без единого расхождения.

Репозиторий проекта с тестами, бенчмарками и отчётом. Источник: github.com/simonw/research
Репозиторий проекта с тестами, бенчмарками и отчётом. Источник: github.com/simonw/research

Сравнение подходов: C против Rust

Анализ характеристик расширений показал различия между подходами:

  • Размер скомпилированного файла на C составляет 14 КБ (из которых реальный код занимает 3,5 КБ, а остальное приходится на выравнивание страниц ELF). Файлы на Rust весят 284–321 КБ даже с флагом opt-level = "z" из-за включения стандартной библиотеки и механизмов обработки паник.
  • Расширение на C компилируется за 0,1 секунды через GCC и зависит только от системной libc. Сборка на Rust требует Cargo, скачивания от 20 до 62 крейтов и занимает от 11 до 72 секунд.
  • Реализация на Rust компактнее и безопаснее в написании: управление памятью, срезами строк и переполнением целых чисел берёт на себя компилятор. Версия на C потребовала ручной обработки буферов и специального условия для INT64_MIN.
  • Быстрый путь Клингера на C отключается во время компиляции, если макрос FLT_EVAL_METHOD отличается от нуля, чтобы исключить риск двойного округления на 80-битных регистрах x87.

Для сборки быстрого C-расширения используется стандартная команда компилятора:

gcc -O2 -fPIC -shared c-fast/parse_number.c -o parse_number.so

Саймон Уиллисон развивает Datasette и экосистему расширений SQLite с 2017 года. Эксперимент продемонстрировал способность Claude Opus 5.5 генерировать рабочий системный код и проводить низкоуровневую алгоритмическую оптимизацию под жесткие требования по скорости.

Всё самое интересное — в нашем Telegram-канале «Claude без воды».