Добавить в корзинуПозвонить
Найти в Дзене
Цифровая Переплавка

Как LLM помогает и не помогает оптимизировать код

Когда мы говорим о больших языковых моделях (LLM) и генерации программного кода, часто подразумеваем, что нейросеть может упростить жизнь программистам. Но насколько она действительно хороша в оптимизации кода? Недавняя статья от Дэвида Г. Андерсена (David G. Andersen) даёт интересный пример того, как LLM-подсказки и классический человеческий разум могут по-разному справляться с задачами производительности. Задача, которую изначально решал Макс Вулф (Max Woolf):
«Дан список из 1 миллиона случайных целых чисел от 1 до 100 000. Нужно найти разницу между минимальным и максимальным значениями, сумма цифр которых равна 30». Код на Python, сгенерированный LLM (GPT и Copilot), выглядел примерно так: import random
# Генерация списка из миллиона чисел
random_integers = [random.randint(1, 100000) for _ in range(1000000)]
def digit_sum(n):
return sum(int(digit) for digit in str(n))
filtered_numbers = [num for num in random_integers if digit_sum(num) == 30]
if filtered_numbers:
min_num
Оглавление

Когда мы говорим о больших языковых моделях (LLM) и генерации программного кода, часто подразумеваем, что нейросеть может упростить жизнь программистам. Но насколько она действительно хороша в оптимизации кода? Недавняя статья от Дэвида Г. Андерсена (David G. Andersen) даёт интересный пример того, как LLM-подсказки и классический человеческий разум могут по-разному справляться с задачами производительности.

Проблема на миллион чисел

Задача, которую изначально решал Макс Вулф (Max Woolf):
«Дан список из 1 миллиона случайных целых чисел от 1 до 100 000. Нужно найти разницу между минимальным и максимальным значениями,
сумма цифр которых равна 30».

Код на Python, сгенерированный LLM (GPT и Copilot), выглядел примерно так:

import random

# Генерация списка из миллиона чисел
random_integers = [random.randint(1, 100000) for _ in range(1000000)]

def digit_sum(n):
return sum(int(digit) for digit in str(n))

filtered_numbers = [num for num in random_integers if digit_sum(num) == 30]

if filtered_numbers:
min_number = min(filtered_numbers)
max_number = max(filtered_numbers)
difference = max_number - min_number
print(f"Difference: {difference}")
else:
print("No numbers found.")

На Mac M1 это занимало ~520 миллисекунд. С одной стороны, уже неплохо для «прямолинейного» Python. Но очевидно, что через NumPy, Numba и прочие библиотеки можно ускорить процесс. Однако интересно было посмотреть, как этот же код поведёт себя в Rust.

Переход на Rust: 12-кратное ускорение

LLM предложила аналогичный «стилем» код на Rust:

use rand::Rng;

fn digit_sum(n: i32) -> i32 {
n.to_string().chars().map(|c| c.to_digit(10).unwrap() as i32).sum()
}

fn main() {
let mut rng = rand::thread_rng();
let numbers: Vec<i32> = (0..1_000_000).map(|_| rng.gen_range(1..=100_000)).collect();

let mut min_number = i32::MAX;
let mut max_number = i32::MIN;

for &number in &numbers {
if digit_sum(number) == 30 {
if number < min_number {
min_number = number;
}
if number > max_number {
max_number = number;
}
}
}
// ...
}

Уже на этой стадии программа выполняется за 42 мс. То есть прирост в 12 раз по сравнению с Python. Но можно улучшать дальше.

Оптимизация digit_sum

⚙️ Первое улучшение: не преобразовывать число в строку, а складывать цифры напрямую, разделяя число целочисленным делением и получая остаток:

fn digit_sum(n: i32) -> i32 {
let mut sum = 0;
let mut x = n;
while x > 0 {
sum += x % 10;
x /= 10;
}
sum
}

Это уменьшило время до ~13 мс, плюс-минус 3-кратное ускорение!

Простой, но «забытый» трюк

Многие коды, сгенерированные LLM, сначала проверяют сумму цифр, а уж потом корректируют min_number и max_number. Но есть обратная логика:

«Вычислять digit_sum только тогда, когда это действительно нужно.»

Например, если текущий number уже не может быть меньше min_number или больше max_number, зачем проверять сумму цифр? Для Python такое улучшение давало 5-кратный рост. В Rust (где сам digit_sum уже сильно ускорен) выигрыш меньше — ~1.2x, но всё равно экономит время.

Сюрприз: генерация случайных чисел

В Rust пакет rand (по умолчанию) использует криптостойкий генератор случайных чисел. Он медленнее, чем простые алгоритмы. Опытный разработчик замечает, что в задаче не нужны криптографические свойства, поэтому можно взять нечто вроде fastrand и получить ещё один выигрыш. LLM автоматически это не подсказывает — приходится самостоятельно:

⚙️ Заменили rand на fastrand:

let numbers: Vec<i32> = (0..1_000_000).map(|_| fastrand::i32(1..=100_000)).collect();

Время упало до 2.8 мс. Ещё в 3–4 раза быстрее!

Параллелизация

Наконец, можно распараллелить генерацию и обработку. LLM с помощью rayon что-то предложит, но может столкнуться с проблемами совместного доступа к переменным min_number и max_number. Здесь программисту придётся идти на комбинированные ухищрения:

🫴 Сначала найти промежуточные min_number/max_number на небольшой выборке
🫴
Параллельно генерировать остальные числа и фильтровать их по сумме цифр и тому, выходят ли они за текущие мин/макс
🫴
Ещё раз уточнить итоговые min_number и max_number последовательным проходом

Подобные «умные» схемы — уже зона, где LLM «спотыкается», потому что она не умеет сама анализировать профилировщик и хитро жонглировать порядком вычислений. Результат: время снизилось до ~760 микросекунд. Сравните с 13 мс у простого варианта — наглядный пример, как вдумчивые (а не только «в лоб») оптимизации творят чудеса.

Личное мнение

На мой взгляд, этот эксперимент показывает, что LLM хорошо справляется с тривиальными улучшениями(например, упростить функцию digit_sum, применить очевидную параллельную библиотеку). Но тонкие оптимизации, требующие:

🦾 понимания контекста и порядка операций,
🦾
учёта реальных bottleneck’ов в профайлере,
🦾
снижения избыточных действий,

— всё ещё остаются уделом «человеческого фактора». Это не «провал» нейросетей, а лишь признак того, что алгоритмическая оптимизация — сложная область, где нужен живой разум, умеющий экспериментировать и проверять догадки.

С другой стороны, это не делает LLM менее полезными. Напротив, они могут сэкономить массу времени на рутине (рефакторинг, параллелизация, набросок кода), а дальше живой программист доведёт решение до ума.

Ссылки и материалы