Подробные статьи о технологиях, определяющих будущее.

Правила программирования Роба Пайка актуальны до сих пор

Роб Пайк записал пять правил программирования в 1989 году. Сегодня они актуальнее, чем тогда, особенно те, что мы продолжаем игнорировать.

Старая карточка с правилами, приколотая рядом с захламленным современным столом под одной лампой

В 1989 году Роб Пайк, позже ставший соавтором Go, UTF-8 и Plan 9, записал пять правил программирования. Они достаточно коротки, чтобы уместиться на индексной карточке, и при этом настолько содержательны, что программисты спорят о них уже 37 лет. Большинство разработчиков встречали хотя бы некоторые из них в блогах или на конференциях. Гораздо меньше тех, кто действительно усвоил их, и это жаль: правила сэкономили бы кучу напрасных усилий.

Правила кажутся обманчиво простыми. Они не про синтаксис и не про архитектурные паттерны. Они о том, где программисты раз за разом теряют время, и о том, как перестать это делать.

Пять правил

Прежде чем разбирать их, сформулирую их прямо:

  1. Вы не можете предсказать, где программа будет тратить время. Узкие места возникают в неожиданных местах, поэтому не пытайтесь угадывать и не добавляйте ускоряющие хаки, пока не доказали, что узкое место именно здесь.
  2. Измеряйте. Не настраивайте скорость, пока не измерили, и даже тогда не делайте этого, если одна часть кода не перевешивает остальные.
  3. Изощрённые алгоритмы медленны при малом n, а n обычно мало. У изощрённых алгоритмов большие константы. Пока вы не знаете, что n часто будет большим, не усложняйте.
  4. Изощрённые алгоритмы содержат больше багов, чем простые, и их значительно сложнее реализовать. Используйте простые алгоритмы и простые структуры данных.
  5. Данные решают всё. Если вы выбрали правильные структуры данных и хорошо их организовали, алгоритмы почти всегда становятся очевидными. Центральную роль в программировании играют структуры данных, а не алгоритмы.

Правила 1 и 2 касаются оптимизации. Правила 3 и 4 касаются сложности. Правило 5 касается дизайна. Вместе они образуют философию, в основе которой лежит смирение: признание того, что наши интуитивные представления о производительности ошибочны, что сложность стоит дороже, чем мы думаем, и что хорошие структуры данных важнее хитроумного кода.

Правило 1: вы не знаете, где узкое место

Это правило разработчики нарушают увереннее всего. «Я знаю, что эта функция медленная, потому что в ней вложенный цикл». «Тут надо использовать хеш-таблицу, ведь поиск за O(1)». «Я выделю этот массив заранее, потому что аллокация дорогая». Звучит разумно. Но часто это неверно.

Я профилировал достаточно продакшен-систем, чтобы собрать коллекцию примеров, где интуитивное узкое место оказывалось вовсе не настоящим. Система, где все считали узким местом базу данных, а профилирование показало, что сериализация JSON съедала 60% времени запроса. Конвейер обработки данных, где «дорогое» матричное умножение занимало 5% времени выполнения, а парсинг CSV — 70%. Веб-приложение, где команда месяцами оптимизировала запросы к базе, в то время как настоящим узким местом было DNS-разрешение при каждом исходящем HTTP-запросе.

Человеческий мозг — ужасный профилировщик. Мы переоцениваем операции, которые кажутся концептуально дорогими (запросы к базе, сетевые вызовы), и недооцениваем те, что кажутся дешёвыми (конкатенация строк, парсинг JSON, выделение памяти). Современное железо усугубляет ситуацию: кэши процессора, предсказание ветвлений и внеочередное исполнение означают, что связь между сложностью кода и временем выполнения крайне неинтуитивна.

Правило 2: сначала измерьте, потом оптимизируйте

Это практическое следствие правила 1. Не оптимизируйте на основе интуиции. Профилируйте. Найдите реальную горячую точку. Затем оптимизируйте именно её и только её.

Вторая половина этого правила, «не делайте этого, если одна часть кода не перевешивает остальные», не менее важна, но её цитируют реже. Если профилировщик показывает, что время выполнения равномерно распределено между 20 функциями, каждая из которых занимает по 5%, единственного узкого места нет. Ускорение одной функции в 2 раза сэкономит всего 2,5% общего времени. Обычно это не стоит добавленной сложности. Нужен принципиально другой подход, а не оптимизация отдельных функций.

# Profile first. Always.
# Python
python -m cProfile -s cumulative my_script.py
# Node.js
node --prof app.js
node --prof-process isolate-*.log > profile.txt
# Go
go test -bench=. -cpuprofile=cpu.prof
go tool pprof cpu.prof
# Rust
cargo flamegraph
# General (Linux)
perf record ./my_program && perf report
# The flamegraph (brendangregg.com/flamegraphs) is the most
# informative visualization. It shows you exactly where time
# is spent, in a format that makes bottlenecks visually obvious.

Правило 3: изощрённые алгоритмы медленны, когда N мало

Это правило, которое в образовании по информатике подают наоборот. Нам говорят, что O(n log n) лучше, чем O(n²), и асимптотически это верно. Но при n = 20 хорошо реализованная сортировка вставками за O(n²) быстрее сортировки слиянием за O(n log n) из-за констант, поведения кэша и накладных расходов.

Примеров из реальной жизни сколько угодно. Линейный поиск по отсортированному массиву из 50 элементов быстрее бинарного, потому что у линейного поиска идеальная локальность кэша и нет промахов предсказания ветвлений. Простой связный список обгоняет сбалансированное двоичное дерево для коллекций из менее чем ~100 элементов, поскольку обход указателей по дереву разрушает локальность кэша. У хеш-таблиц амортизированный поиск за O(1), но константа настолько велика, что линейный поиск по массиву быстрее для коллекций меньше ~30–50 элементов.

Стандартные библиотеки это учитывают. Функция sorted() в Python использует Timsort, который для небольших подпоследовательностей переходит на сортировку вставками. std::sort в C++ переключается на сортировку вставками ниже порога (обычно 16–32 элемента). sort_unstable в Rust сочетает быструю сортировку и сортировку вставками. «Изощрённый» алгоритм используется только там, где n действительно достаточно велико, чтобы он выигрывал.

Более общий вывод: знайте свой n. Если выбираете между простым алгоритмом O(n²) и сложным O(n log n), спросите себя, каким будет n на практике. Если он меньше нескольких сотен, простой алгоритм почти наверняка подойдёт, и его будет проще написать, отлаживать и поддерживать.

Правило 4: простое лучше умного

Правило 4 распространяет правило 3 за пределы производительности. Изощрённые алгоритмы не просто медленнее при малом n, они ещё и опаснее с точки зрения багов. В красно-чёрном дереве больше граничных случаев, чем в отсортированном массиве. Конкурентная структура данных без блокировок имеет более тонкие режимы отказа, чем структура с мьютексом. Самописный аллокатор памяти может испортить память большим числом способов, чем системный.

Я видел, как команды неделями писали и отлаживали самодельный LRU-кэш с операциями за O(1), хотя простой ограниченный массив с линейным вытеснением написали бы за полдня, он работал бы правильно с первого раза и был бы достаточно быстрым для их нагрузки (там было не больше нескольких сотен записей в кэше).

Стоимость сложности не ограничивается первоначальной реализацией. Она ложится на каждого будущего разработчика, которому придётся понимать, менять и отлаживать код. Простой алгоритм, который понимает вся команда, ценнее умного, который может поддерживать только автор. А автор через полгода — это фактически другой человек, который к тому же забыл, как всё работает.

Отлаживать код вдвое сложнее, чем писать его в первый раз. Поэтому, если писать код максимально хитроумно, вы по определению недостаточно умны, чтобы его отладить. — Брайан Керниган

Правило 5: данные решают всё

Это самое важное правило Пайка и то, которое чаще всего упускают разработчики, увлечённые алгоритмами и паттернами проектирования. Суть в следующем: если структуры данных выбраны правильно, алгоритмы возникают сами собой. Если структуры данных неверны, никакая алгоритмическая изобретательность не спасёт.

Фред Брукс сказал нечто похожее: «Покажите мне свои блок-схемы и скройте таблицы, и я по-прежнему буду в недоумении. Покажите мне таблицы, и блок-схемы мне, как правило, не понадобятся». Линус Торвальдс вторил ему: «Плохие программисты беспокоятся о коде. Хорошие программисты беспокоятся о структурах данных и их взаимосвязях».

Этот принцип постоянно проявляется на практике. Кодовая база, которая хранит права пользователей плоским списком строк-разрешений, со временем обрастает сложной и подверженной ошибкам логикой проверок, разбросанной по всему коду. Перестройте данные в виде иерархии ролей, и логика проверок станет тривиальной. Система, которая хранит события как JSON-блобы, потребует сложного парсинга и валидации в каждом потребителе. Опишите события как типизированные записи с явными схемами, и потребители заметно упростятся.

# Bad data structure → complex algorithm
class OrderSystem:
def __init__(self):
self.orders = []  # flat list of all orders
def get_user_orders(self, user_id):
return [o for o in self.orders if o.user_id == user_id]  # O(n)
def get_pending_orders(self):
return [o for o in self.orders if o.status == 'pending']  # O(n)
def get_user_pending_orders(self, user_id):
return [o for o in self.orders
if o.user_id == user_id and o.status == 'pending']  # O(n)
# Better data structure → algorithms become obvious
class OrderSystem:
def __init__(self):
self.orders_by_user = {}      # user_id → [orders]
self.orders_by_status = {}    # status → [orders]
def get_user_orders(self, user_id):
return self.orders_by_user.get(user_id, [])  # O(1)
def get_pending_orders(self):
return self.orders_by_status.get('pending', [])  # O(1)
# The right data structure makes the code self-evident.
# You don't need to think about algorithms at all.

Как Go воплощает эти правила

Глядя на правила Пайка, трудно не увидеть философию дизайна Go в зачаточном виде. Go, который Пайк создал через 20 лет после написания этих правил, это язык, систематически отдающий предпочтение простоте перед хитроумием.

  • Никаких дженериков (поначалу): они заставляют использовать простые структуры данных. (Дженерики появились в Go 1.18, но лишь после многих лет сопротивления, пока не нашли достаточно простой дизайн.)
  • Никакой перегрузки операторов: код означает то, что выглядит.
  • Никаких неявных преобразований типов: явность важнее хитрости.
  • Никаких исключений: обрабатывайте ошибки там, где они возникают.
  • Минимум стандартных алгоритмов: используйте срезы и карты, а не изощрённые структуры данных.
  • Встроенное профилирование (pprof): измеряйте, а не гадайте.

Go часто критикуют за «скуку» разработчики, которые предпочитают более выразительные языки. Но в этом и суть. Правила Пайка это рецепт скучного кода: простого, измеримого и построенного на хороших структурах данных, а не на хитроумных алгоритмах. Go это то, что получается, когда этот рецепт превращают в язык.

Где правила не работают

Ни один набор правил не универсален, и у правил Пайка есть законные исключения. Системы, критичные к производительности (игровые движки, внутренности баз данных, компиляторы), иногда нуждаются в изощрённых алгоритмах, потому что их n действительно велико. Инфраструктурный код, который выполняется миллионы раз в секунду, оправдывает оптимизации, которые не нужны прикладному коду. А иногда «простой» алгоритм имеет сложность O(n³), которая недопустима даже при скромных n.

Правила это эвристики, а не законы. Их ценность в том, что они корректируют распространённое смещение: разработчики склонны оптимизировать слишком рано, использовать слишком сложные алгоритмы и слишком много думать о коде и слишком мало о структурах данных. Правила Пайка противодействуют этим тенденциям. Если вы оказались в редкой ситуации, когда действует противоположное смещение и действительно нужно больше сложности, а не меньше, смело усложняйте. Но сначала измерьте.

Через тридцать семь лет после того, как Пайк записал их, эти правила остаются одним из лучших советов по программированию, когда-либо опубликованных. Не потому, что они удивительны: большинство опытных разработчиков, читая их, думают «да, очевидно». Ценность в том, что они сформулированы достаточно ясно, чтобы применять их последовательно. В следующий раз, когда потянетесь к красно-чёрному дереву, самописному аллокатору или «оптимизации», которую вы не профилировали, вспомните: сначала измерьте, держите всё простым и выбирайте правильные структуры данных.