Линейная алгебра, графики, метапрограммирование и параллелизм

Обзорный тур по продвинутым возможностям Julia: линейная алгебра, графики, макросы и параллельные вычисления.

В этом уроке мы пройдёмся по четырём областям, где Julia особенно сильна, чтобы вы знали, куда копать дальше.

Линейная алгебра — встроенная

Стандартный модуль LinearAlgebra даёт операции, для которых в Python нужен NumPy/SciPy. Решение систем, определители, собственные значения, разложения — всё доступно сразу:

using LinearAlgebra

A = [2.0 1.0; 1.0 3.0]
b = [3.0, 5.0]

x = A \ b          # решить систему Ax = b
println(det(A))    # определитель
println(tr(A))     # след матрицы
vals = eigvals(A)  # собственные значения

Оператор обратной косой черты \ — это «решить систему уравнений», читаемо и эффективно. Под капотом он выбирает подходящий численный метод в зависимости от свойств матрицы.

Графики: Plots.jl

Пакет Plots.jl — единый интерфейс к нескольким «движкам» отрисовки. Базовый график строится так (нужен установленный пакет):

using Plots

x = 0:0.1:2π
y = sin.(x)            # broadcasting: синус каждой точки
plot(x, y, label="sin(x)")
savefig("sine.png")

Снова виден broadcasting: sin.(x) применяет синус к каждой точке диапазона.

Метапрограммирование и макросы

Вы уже пользовались макросами: @btime, @code_warntype. Макрос — это «функция над кодом»: он принимает не значения, а само выражение и преобразует его до выполнения. Так работает, например, @time, оборачивающий выражение замером времени:

@time sum(1:1000000)

Метапрограммирование (работа с кодом как с данными, тип Expr) позволяет авторам пакетов создавать удобные «мини-языки» внутри Julia — например, @. автоматически расставляет точки во всём выражении, а JuMP даёт синтаксис для записи задач оптимизации. Это мощно, но для начинающих — инструмент авторов библиотек, а не повседневный.

Параллелизм и распределённость

Julia изначально создавалась для высокопроизводительных вычислений, поэтому параллелизм встроен на нескольких уровнях:

  • SIMD — векторизация на уровне процессора (часто автоматическая для стабильного кода).
  • Многопоточность — макрос Threads.@threads распараллеливает цикл по ядрам.
  • Распределённость — модуль Distributed и @distributed для нескольких процессов и машин.
  • GPU — пакеты CUDA.jl и др. позволяют считать на видеокарте почти тем же кодом.
using Base.Threads

results = zeros(8)
@threads for i in 1:8
    results[i] = i^2     # каждая итерация — в своём потоке
end

Как работает под капотом

Все эти возможности опираются на одну основу — систему типов и компиляцию через LLVM. Тот же механизм, что делает обычный код быстрым, позволяет генерировать SIMD-инструкции, GPU-ядра и специализированные численные методы. Линейная алгебра, в свою очередь, опирается на проверенные библиотеки BLAS/LAPACK (как NumPy), но интегрирована в язык через множественную диспетчеризацию: один оператор \ выбирает метод по типу матрицы.

Частые ошибки

С многопоточностью главная ловушка — гонки данных: если несколько потоков пишут в одну и ту же переменную без защиты, результат непредсказуем. В примере выше каждый поток пишет в свою ячейку results[i] — это безопасно. С метапрограммированием частая ошибка новичков — пытаться писать макросы там, где хватило бы обычной функции; макрос нужен, только когда вы реально преобразуете код, а не значения.

Итоги

  • Линейная алгебра встроена (LinearAlgebra): A \ b, det, eigvals.
  • Plots.jl — единый интерфейс для графиков; точечный синтаксис строит данные кривой.
  • Макросы (@time, @btime) — это преобразования кода; метапрограммирование — инструмент авторов пакетов.
  • Параллелизм встроен: SIMD, Threads.@threads, Distributed, GPU.
  • Всё опирается на единую основу — типы и LLVM-компиляцию.
Проверьте себя
1. Что делает оператор \ в выражении x = A \ b с матрицей A и вектором b?
AДелит каждый элемент A на b
BРешает систему линейных уравнений Ax = b
CТранспонирует матрицу A
DОбъединяет A и b в одну строку
2. Чем макрос (например, @time) отличается от обычной функции?
AМакрос работает быстрее любой функции
BМакрос получает само выражение кода и преобразует его до выполнения
CМакрос можно вызвать только один раз
DМежду ними нет разницы
3. Почему запись results[i] = i^2 внутри @threads-цикла безопасна?
AПотоки в Julia не существуют
BКаждый поток пишет в свою отдельную ячейку results[i], без гонки данных
CThreads автоматически блокирует переменные
DПотому что используется broadcasting