Работаю в сфере научных вычислений уже больше десяти лет, и до сих пор каждый раз удивляюсь, когда вижу строчки на Fortran в современных HPC-проектах. Казалось бы, язык из семидесятых, а продолжает определять то, как именно компиляторы выжимают производительность из процессоров. Честно говоря, долгое время считал это просто данью традиции. Пока не попробовал разобраться, почему именно императивная структура Fortran 77 оказалась настолько удачной для векторных оптимизаций, что её следы видны в самых передовых системах.

Почему циклы из прошлого работают лучше, чем код из будущего

Fortran остается популярным в научных кругах, особенно в суперкомпьютерном моделировании, благодаря своим возможностям векторизации. Но что такого особенного в том, как писали код почти полвека назад?

В семидесятые годы вычислительная техника развивалась совершенно иначе, чем сейчас. Тогда не было объектно-ориентированного программирования, абстракций данных, шаблонов проектирования. Fortran 77 добавил поддержку структурного программирования и обработки символьных данных, и это было большим шагом вперед. Императивный стиль, который сейчас многие считают архаичным, на самом деле заставлял программистов мыслить предельно конкретно: есть массив, есть цикл, есть последовательность операций над элементами.

Когда смотришь на типичный фортрановский цикл DO, видишь кристально чистую логику: индекс идет от начального значения до конечного с определенным шагом, внутри цикла выполняются операции с элементами массивов. Никаких скрытых побочных эффектов, никаких неявных вызовов конструкторов. Компилятор видит всю картину целиком и понимает: эти операции независимы друг от друга. А значит, их можно выполнить параллельно, можно обработать несколько элементов за один такт процессора.

Как секции массивов изменили подход к векторизации

В Fortran есть конструкция секции массива, которая позволяет легко описывать идею векторизации: если элементы массива находятся последовательно друг за другом или лежат с константным шагом. Это был прорыв, хотя в момент появления мало кто понимал его масштаб.

Суть в том, что вместо написания цикла вида "для каждого i от 1 до N выполни операцию над элементом A(i)" можно было записать просто "A(1:N) = B(1:N) + C(1:N)". Кажется мелочью? Но для компилятора это совершенно разные вещи. В первом случае он должен доказать, что итерации цикла независимы, проанализировать возможные зависимости по данным, убедиться, что нет конфликтов обращений к памяти. Во втором случае все эти гарантии даны явно самой семантикой языка.

После векторизации получается сокращение количества итераций, и программа оперирует уже не элементами массива, а секциями массива. Процессор перестает работать с одним числом за раз, он начинает обрабатывать пакеты данных. Четыре числа одной командой, восемь чисел, шестнадцать. Современные AVX-512 инструкции в процессорах Intel позволяют обрабатывать сразу по 512 бит данных, то есть 16 чисел с плавающей точкой одинарной точности или 8 чисел двойной точности.

Структура памяти и предсказуемость доступа

Тут проявляется еще одна особенность Fortran 77, о которой обычно не задумываются. Массивы в этом языке хранятся в памяти по столбцам (column-major order), в отличие от C, где используется построчное хранение (row-major order). Для векторных операций это критически важно.

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

Наверняка замечали, что библиотеки линейной алгебры, вроде BLAS или LAPACK, написаны на Fortran? LAPACK - библиотека линейной алгебры, на которой держится половина научных расчетов в мире, написана на Fortran. И дело не только в историческом наследии. Дело в том, что организация данных в Fortran естественным образом ложится на векторные операции.

От векторных процессоров Cray к современным SIMD

В 1978 году для Cray-1 был выпущен Cray Fortran (CFT), первый компилятор Fortran с автоматической векторизацией. Это была настоящая веха. До этого программисты должны были вручную адаптировать код под векторные команды процессора. Представьте: пишешь обычный цикл, а компилятор сам преобразует его в последовательность векторных инструкций.

Суперкомпьютеры Cray были построены на векторных процессорах, где одна команда могла обработать целый вектор данных. Fortran 77 с его простой императивной структурой идеально подходил для такой архитектуры. Компилятор мог четко определить границы циклов, проверить независимость итераций, выстроить конвейер обработки данных.

Сегодня векторные процессоры как отдельный класс почти исчезли, но их идеи живут в SIMD-расширениях обычных процессоров. SSE, AVX, AVX-512 в процессорах Intel и AMD, NEON в ARM-процессорах. Все это векторные инструкции, позволяющие одной командой обработать несколько элементов данных. И все это работает лучше всего именно с тем стилем кода, который был заложен в Fortran 77.

Как компиляторы превращают простоту в скорость

Современные компиляторы, такие как NVIDIA HPC Fortran, позволяют автоматически распараллеливать и векторизовать код программ, в том числе циклы do concurrent, как на ядра SMP, так и на графические ускорители. Причем автоматически. Не нужно переписывать программу под CUDA, не нужно вручную разбираться с управлением памятью GPU.

Почему это работает? Потому что императивная структура Fortran 77 дает компилятору все необходимые гарантии. Когда видишь цикл DO с фиксированными границами и арифметическими операциями над массивами, можешь быть уверен: здесь нет скрытых вызовов функций, нет виртуальных методов, нет указателей, которые могут ссылаться куда угодно. Все прозрачно, все предсказуемо.

Современные компиляторы, вроде Intel Fortran или GNU Fortran, используют сложнейшие алгоритмы анализа зависимостей. Они строят граф зависимостей по данным, определяют, какие части кода могут выполняться параллельно, какие требуют синхронизации. И делают это с поразительной точностью именно на фортрановском коде, потому что семантика языка работает на них.

Наследие в современных параллельных вычислениях

Когда разбираешь, как устроены OpenMP директивы или MPI библиотеки для параллельных вычислений, видишь те же паттерны, что и в Fortran 77. Явное указание границ параллельных регионов, четкое разделение данных между потоками, предсказуемые шаблоны доступа к памяти.

Программисты часто пишут логически тяжелые вычисления на Fortran, а интерфейс и интеграцию делают на Python. Это стало стандартной практикой в научных вычислениях. Python отвечает за препроцессинг данных, за визуализацию, за взаимодействие с пользователем. Fortran делает то, что умеет лучше всего - считает.

Более того, многие современные численные библиотеки для Python, такие как NumPy или SciPy, внутри вызывают именно фортрановские библиотеки. Когда запускаешь операцию умножения матриц в NumPy, под капотом работает BLAS, написанная на Fortran. Круг замкнулся: современные высокоуровневые языки опираются на код, написанный по канонам полувековой давности.

Почему это все еще имеет значение

Можно подумать: ну хорошо, был такой язык, была такая история, но при чем тут современные HPC-системы? Разве нельзя написать эффективный код на C++ или Rust? Технически можно, но сложнее. Гораздо сложнее.

В большинстве тестов Fortran и C/C++ оказываются самыми быстрыми языками, но Fortran выигрывает в автоматической оптимизации. В C++ нужно тщательно контролировать алиасинг указателей, использовать ключевое слово restrict, помнить про strict aliasing rule. В Fortran эти проблемы решены на уровне семантики языка.

Когда пишешь вычислительное ядро для симуляции молекулярной динамики или для предсказания погоды, хочешь сконцентрироваться на физике задачи, а не на особенностях компилятора. И вот тут императивная структура Fortran 77, с ее простыми циклами и прозрачной работой с массивами, оказывается именно тем, что нужно.

Современные суперкомпьютеры обрабатывают петафлопсы операций в секунду. Это квадриллионы вычислений. Каждый лишний процент эффективности переводится в реальное время расчетов, в реальные деньги, в реальные результаты. И когда видишь, что код на Fortran работает на 10-15% быстрее эквивалентного кода на C++ просто благодаря лучшей векторизации, понимаешь, почему язык все еще жив.

Разговаривал недавно с коллегой, который занимается климатическими моделями. Он сказал фразу, которая застряла в голове: "Fortran не устарел, потому что процессоры изменились, чтобы соответствовать ему". И в этом есть доля правды. Когда Intel или AMD разрабатывают новые векторные расширения, они тестируют их на реальном коде. А большая часть научного кода - это Fortran. Получается эволюция в обе стороны: язык влияет на архитектуру, архитектура влияет на язык.

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