← Назад к новостям
Swift 6.2 против вашего оптимизатора: разбираемся с InlineArray, Span и strict memory safety на замерах
📰 Habr
📅 07.07.2026 11:53
⏱️ 28 мин
Про ARC, weak / unowned и retain cycle на русском написано столько, что добавить нечего. Про borrowing / consuming и ~Copyable (это, к слову, Swift 5.9 , а не 6 - частая путаница) на английском тоже десятки разборов. И почти все они устроены одинаково: объясняют, что фича делает, на игрушечном примере с файловым дескриптором, и заканчивают выводом «так безопаснее и быстрее». Мне захотелось зайти иначе. Я взял три «памятных» нововведения Swift 6.2, написал на них бенчмарки, прогнал на релизном тулчейне и полез в сгенерированный ассемблер. Вопрос меня занимал не «что это делает», а куда более практичный: дают ли эти фичи реальный выигрыш в цифрах - и в каких случаях оптимизатор Swift давно делает то же самое за вас, бесплатно и молча. Ответ оказался менее очевидным, чем их маркетинг. Статью я старался написать так, чтобы её читал не только стафф. Поэтому по ходу я коротко объясняю базовые вещи - стек и кучу, ARC, copy-on-write. Если вы это и так знаете, просто пролистывайте врезки «на полях». О чём это и почему не «ещё одна статья про ARC» Swift 6.2 принёс три вещи, которые напрямую касаются памяти: InlineArray - массив фиксированного размера, который хранит элементы прямо внутри себя (обычно на стеке), без отдельной аллокации в куче, без copy-on-write и без подсчёта ссылок. Опирается на value generics - это когда параметр дженерика не тип, а число. Span / MutableSpan - безопасное невладеющее «окно» в непрерывную область памяти, замена unsafe-указателям. Под капотом - non-escapable типы ( ~Escapable ) и lifetime dependencies. Строгая memory safety (opt-in) - режим компилятора, который подсвечивает небезопасные конструкции. Это не про скорость, это про диагностику. Главный тезис, который я держу до самого конца: самое интересное - там, где фича не даёт выигрыша. Потому что во многих случаях компилятор уже разместил ваш массив на стеке сам и уже убрал лишний подсчёт ссылок. Никаких «X всегда быстрее» здесь не будет. На полях: стек и куча, если совсем коротко. Стек - это быстрая «стопка тарелок» на время работы функции: положили сверху, по выходу из функции сняли, всё автоматически. Куча (heap) устроена как общий «склад»: туда кладут то, что должно жить дольше одной функции. Но за это платят: аллокация в куче дороже, а за временем жизни объекта кто-то должен следить (в Swift этим занимается ARC, о нём ниже). Если совсем грубо: стек дёшев и прост, куча дороже. Поэтому «убрать поход в кучу» и звучит как выигрыш - вот это мы и будем проверять. Раздел 0. Как я мерил (не пропускайте - иначе цифрам нельзя верить) Микробенчмарки врут по умолчанию. Наносекундные замеры чувствительны ко всему: к режиму сборки, к тому, не выкинул ли оптимизатор ваш код целиком, к случайному шуму системы. Поэтому прежде чем показывать числа, зафиксируем условия - иначе это будут красивые, но бессмысленные цифры. Окружение Все замеры сделаны на одной машине. Полный код и скрипт, который повторяет весь прогон одной командой, лежат в репозитории (ссылка в конце). Параметр Значение Swift Apple Swift 6.2.4 (swiftlang-6.2.4.1.4 clang-1700.6.4.2) Xcode 26.3 (17C529) ОС macOS 26 (Darwin 25.3.0), arm64 Чип Apple M4 Pro , 24 ГБ RAM Флаги сборки -O (release) Гарнитура собственный харнесс с перцентилями, без внешних зависимостей Отдельно про доступность: часть API вокруг InlineArray / Span привязана к свежим версиям appleOS. На Linux со свежим тулчейном собирается многое, но не всё и не со всеми conformance’ами. Так что если вы гоняете это на другой платформе - проверяйте отдельно, картина может отличаться. Три правила, без которых замер недействителен 1. Только -O (release). В debug-сборке ( -Onone ) картина бессмысленная: там ARC и аллокации раскиданы повсюду, и InlineArray «выигрывает» у Array по совершенно случайным причинам. Мерить нужно то, что уедет в прод, - релизную сборку. 2. Не дать оптимизатору съесть ваш бенчмарк. Представьте: вы в цикле создаёте массив, но нигде его не используете. Оптимизатор в режиме -O не дурак - он видит, что результат никому не нужен, и просто удаляет весь ваш код. Вы замеряете пустой цикл и радуетесь «нулю наносекунд». Чтобы этого не случилось, результат нужно «спрятать» от оптимизатора - для этого есть приём под названием blackHole . 3. Перцентили, а не среднее. На наносекундных масштабах среднее арифметическое обманывает: одна случайная задержка от планировщика ОС - и среднее поехало. Поэтому я беру перцентили: p50 (медиана, «типичный» случай), p90 и p99 (хвосты, «а как в худшем случае»). Что такое blackHole и зачем он нужен blackHole - это функция-«заглушка», которая убеждает оптимизатор, что результат вычисления кому-то нужен и выбрасывать его нельзя. Трюк в двух вещах: функцию помечают @inline(never) (оптимизатор не видит её тело и вынужден по-настоящему вычислить и передать ей аргумент), а внутри мы ещё и пишем значение в глобальную переменную, чтобы даже дешёвый Int нельзя было «оптимизировать в никуда»: @inline(never) func blackHole (_ value: T) { // оптимизатор не заглядывает внутрь @inline(never)-функции // и вынужден реально вычислить и передать аргумент globalSink = (globalSink &+ 1) & withExtendedLifetime(value) { 1 } } nonisolated(unsafe) var globalSink = 0 Сам замер устроен так: берём samples образцов, в каждом гоняем тело бенчмарка batch раз, делим время на число итераций и получаем наносекунды на операцию (ns/op). Перцентили считаем по образцам. Полный код - в Sources/bench/Harness.swift в репозитории. Санити-чек: а харнессу вообще можно верить? Прежде чем мерить фичи, я проверил сам инструмент на двух заведомо известных случаях - пустое тело (должно быть ~0) и работа, растущая линейно (должна расти линейно): тело ns/op пустое { } 0.72 blackHole ×10 31.7 blackHole ×100 320.0 blackHole ×1000 3073.8 Пустое тело - 0.72 нс, это фактически ноль (накладные расходы самого цикла и часов). А работа масштабируется идеально линейно: ×10 → ×100 → ×1000 даёт 31.7 → 320 → 3074 нс, то есть ~3.1 нс на каждый вызов blackHole . Значит оптимизатор тело не выбрасывает, и харнессу можно доверять. Отсюда важное следствие для чтения таблиц ниже: сам барьер blackHole стоит ~3–4 нс. Если дальше две строки показывают одинаковые ~4 нс, это не значит, что обе операции медленные - просто обе дешевле барьера, и различить их этим замером уже нельзя. Почему я смотрю в ассемблер, а не только на секундомер Здесь ключевая мысль всей статьи. Число ns/op отвечает на вопрос «сколько», но не отвечает на вопрос «почему». А «почему» часто важнее: две реализации могут показать одинаковое время, но по совершенно разным причинам - и понять это можно, только посмотрев, какой машинный код на самом деле сгенерировал компилятор. Поэтому для каждой фичи я делаю не только замер времени, но и заглядываю в оптимизированный ассемблер ( swiftc -O -S ) и в промежуточное представление SIL. Что я там ищу простыми словами: swift_allocObject - значит был поход в кучу (heap-аллокация). swift_initStackObject - значит объект разместили на стеке (та самая «бесплатная» оптимизация). swift_retain / swift_release - операции ARC (подсчёт ссылок). # ищем в оптимизированном ассемблере: пошли ли в кучу или обошлись стеком swiftc -O -S Bench.swift -o - | grep -E 'swift_allocObject|swift_initStackObject' Часть 1. InlineArray: правда ли стек всегда бьёт кучу Начнём с обещания. InlineArray хранит элементы прямо в своём «теле» - на стеке или внутри объемлющей структуры, без отдельной аллокации в куче, без copy-on-write и без подсчёта ссылок. Размер массива становится частью его типа (это и есть value generics - параметр дженерика не тип, а число). Плата за это - массив не умеет расти: var a: InlineArray = [1, 2, 3] // или синтаксический сахар: [3 of Int] a[0] = 10 // ок, меняем элемент // a.append(4) // нельзя: размер зафиксирован в типе // let b: InlineArray = a // ошибка: другой размер = другой тип var copy = a // копия по значению copy[0] = 99 // a[0] при этом не меняется Обещание звучит убедительно: быстрее, потому что не делает того, что делает обычный Array - не ходит в кучу, не считает ссылки, не проверяет copy-on-write. Проверим. Бенчмарк 1.1 - сколько стоит создать массив Рассмотрим сценарий: мы в цикле создаём маленький 4-элементный массив. Я сравнил три варианта: обычный локальный Array , Array , который «убегает» из функции (что это значит, поясню чуть ниже), и InlineArray . Результат - время (ns/op, p50 / p90 / p99): Создание 4-элементного массива p50 p90 p99 Array (локальный) 4.20 4.65 5.46 Array (убегающий в функцию) 3.83 4.06 4.76 InlineArray 4.23 4.48 5.30 По времени - все три неотличимы , все упираются в тот самый «пол» blackHole (~4 нс). Никакого «стек в разы быстрее кучи» на создании нет и в помине. Однако время само по себе тут ничего не объясняет, интересное начинается в ассемблере. Доказательство - что реально сгенерировал компилятор ( swiftc -O -S , считаем аллокации на каждую функцию): Что за код Пошли в кучу? ( swift_allocObject ) Легло на стек? ( swift_initStackObject ) Array , локальный, константный литерал нет нет, свёрнут в константу Array , локальный, значения из рантайма нет нет, «размазан» по регистрам Array , убегает в borrowing-функцию нет да , stack promotion InlineArray нет нет Array , действительно убегает (сохранён в глобал) да нет Array , возвращён наружу да нет И вот тут ассемблер показывает неожиданное. Локальный Array вообще не ходит в кучу. Компилятор либо целиком свернул его в константу, либо «размазал» по процессорным регистрам. Более того, даже когда массив «убегает» в отдельную функцию (я передавал его в @inline(never) -функцию, принимающую [Int] ), компилятор доказывает, что этот массив не переживёт текущий кадр стека, и кладёт его на стек сам ( swift_initStackObject ). Снова без кучи. InlineArray в этих сценариях не даёт ничего, чего оптимизатор уже не сделал бы бесплатно. На полях: что такое «убегает» и что такое stack promotion. Объект «убегает» (escape), если он должен жить дольше, чем функция, в которой создан: например, вы вернули его наружу или сохранили в глобальную/долгоживущую переменную. Если объект не убегает, компилятор может провернуть stack promotion - разместить его на дешёвом стеке вместо дорогой кучи, потому что точно знает: по выходу из функции объект больше никому не нужен. Это давняя оптимизация, и она работает без всякого InlineArray . Чтобы Array по-настоящему сходил в кучу, мне пришлось заставить его и правда убежать - сохранить в глобальную переменную или вернуть из функции. Только тогда в ассемблере появляется swift_allocObject , то есть настоящая heap-аллокация. И только в этом случае InlineArray , который убегать не умеет в принципе, действительно её вырезает. Если свести к одному предложению: InlineArray не «ускоряет создание массива». Он делает явным и гарантированным то, что для локальных массивов компилятор и так делает за вас. Разница появляется только там, где Array вынужден уйти в кучу из-за escape, а InlineArray этого сделать физически не может. Бенчмарк 1.2 - чтение и запись Чтение (сумма 64 чисел): Операция p50 p90 p99 Array - чтение 4.26 4.60 5.54 InlineArray - чтение 4.08 4.51 5.36 Здесь паритет, оба в пределах шума. Для обычного Int так и должно быть: ARC не платит ни тот, ни другой (число - не ссылка, считать нечего), а проверка выхода за границы массива (bounds-check) есть у обоих. Такой «нулевой» результат я специально не прячу: нет разницы - так и напишем. Запись оказалась интереснее: Операция p50 p90 p99 Array - запись 49.9 53.5 57.1 InlineArray - запись 4.4 4.7 5.9 Разница в 11 раз! Но тут легко сделать неверный вывод. Она не про скорость самой записи по индексу - она про copy-on-write. На полях: что такое copy-on-write (COW). Когда вы копируете Array ( let b = a ), Swift не копирует данные сразу, обе переменные пока указывают на один и тот же буфер в куче. Копирование происходит «лениво», только в момент, когда одну из копий начинают менять . Тогда Swift делает «защитную копию» (defensive copy) буфера, чтобы правки одной переменной не задели другую. Это экономит память при чтении, но за первую запись в разделяемый буфер приходится платить полной копией. В моём замере Array копировался из буфера, у которого был ещё один владелец, поэтому первая же запись форсила защитную копию 64 чисел в кучу. InlineArray же копируется целиком по значению на стеке. То есть 49.9 нс - это цена COW-копии, а не индексации. Точнее говорить не « InlineArray пишет в 11 раз быстрее», а « InlineArray не платит за копию там, где Array вынужден скопировать разделяемый буфер». Если у Array единственный владелец, запись на месте будет сопоставима. Бенчмарк 1.3 - InlineArray как поле структуры А в этом сценарии у InlineArray лучшие шансы на заметный отрыв. Возьмём две структуры: struct WithArray { var xs: [Float] } // массив живёт в куче + COW struct WithInline { var xs: InlineArray } // 4 числа лежат прямо внутри структуры Размеры ( MemoryLayout.size ): Тип Размер WithArray 8 байт - только указатель на буфер в куче WithInline 16 байт - четыре Float лежат прямо в структуре WithArray это всего 8 байт указателя, а сами четыре числа лежат в отдельной аллокации в куче. WithInline несёт данные в себе, без второй аллокации вообще. Теперь стоимость копирования структуры целиком (копия плюс одна запись): Операция p50 p90 p99 копия WithArray (retain + COW при записи) 40.3 43.1 53.8 копия WithInline (чистое значение) 4.1 4.3 5.0 Примерно 10× в пользу InlineArray - и вот это уже настоящий выигрыш по памяти. Когда вы копируете структуру с Array внутри, Swift делает retain её буфера (увеличивает счётчик ссылок), а первая же запись форсит защитную копию в кучу. Структура с InlineArray копируется как обычные 16 байт на стеке - ни retain, ни аллокаций. Это и есть сценарий, где InlineArray реально окупается: небольшой фиксированный буфер как поле структуры, которую часто копируют. Однако у этой скорости есть цена, о которой нельзя молчать. InlineArray - не полноценная коллекция в привычном смысле: у него урезанный набор conformance’ов и API, нет динамики ( append / remove ), а размер зашит в тип. Как только вам понадобится растущий массив, или массивы разных размеров в одной коллекции, или богатый Collection-API - InlineArray не подойдёт. Это узкоспециализированный инструмент, а не замена Array . Часть 2. Span и MutableSpan: безопасный буфер без ARC, и почему его нельзя вернуть Span - это невладеющее «окно» в непрерывную область памяти. Он не владеет данными, а только ссылается на чужой буфер, при этом компилятор на этапе компиляции гарантирует, что окно не переживёт этот буфер. По сути это безопасная замена UnsafeBufferPointer - и, по заявлению, без накладных расходов в рантайме. let numbers = [1, 2, 3, 4, 5] let s: Span = numbers.span // окно поверх буфера массива let first = s[0] // безопасный доступ, без копирования var data = [1, 2, 3] var ms: MutableSpan = data.mutableSpan // изменяемое окно ms[0] = 99 Бенчмарк 2.1 - проход по массиву: Array против Span Рассмотрим сценарий, где элементы - не простые числа, а структуры со ссылкой на класс внутри (я назвал такой тип Node ). Это важный случай, потому что как раз тут в дело вступает ARC. На полях: что такое ARC и почему за него платят. ARC (Automatic Reference Counting) - это способ Swift управлять памятью для ссылочных типов (классов). У каждого объекта есть счётчик ссылок: новая ссылка увеличивает его ( retain ), исчезнувшая уменьшает ( release ), на нуле объект удаляется. Каждый retain / release - маленькая атомарная операция, и в горячем цикле их стоимость набегает. Span обещает, что он заимствует элементы, а не владеет ими, поэтому счётчик трогать не должен. @inline(never) func sumArray(_ a: [Node]) -> Int { // проход по обычному массиву var acc = 0 for x in a { acc += x.box.v } return acc } @inline(never) func sumSpan(_ s: Span ) -> Int { // проход через окно, без владения var acc = 0 for i in 0.. Здесь меня подстерегала методическая ловушка, о которой стоит предупредить. Наивная попытка посчитать ARC-операции через grep по SIL обманывает : у всех четырёх вариантов она показывает одинаковые ~5 retain / ~6 release. Оказалось, что это целиком «холодный» код обработки ошибки выхода за границу массива (формирование строки «Index out of range»), одинаковый везде и в настоящем проходе никогда не исполняемый. Считать нужно по ассемблеру, где холодные ветки вынесены в сторону и видно, что процессор выполняет на горячем пути. Доказательство - ассемблер ( swiftc -O -S ): Функция Инструкций ARC-операций Есть путь через мост к NSArray? sumArray([Node]) класс внутри 68 1 (холодный release ) да sumSpan(Span ) класс внутри 32 0 нет sumArrayInt([Int]) простые числа 14 0 нет sumSpanInt(Span ) простые числа 12 0 нет Разберём, что это значит: Во-первых, на горячем пути ARC нет ни у кого. Даже для массива структур со ссылкой внутри компилятор убирает подсчёт ссылок в цикле: цикл только заимствует элементы, а владеет ими всё это время сам массив. То есть популярный тезис « Span убирает ARC, которого не убирает Array » для простого прохода по массиву - миф . Во-вторых, настоящая разница структурная, а не про ARC. Обычный [Node] тащит за собой проверку «а это нативный Swift-массив или мостовой NSArray из Objective-C?» и целую холодную ветку на случай моста. Span - это чистое непрерывное окно, никаких мостов: вдвое меньше кода (32 инструкции против 68). Для простых чисел даже этой разницы почти нет (12 против 14). На полях: что за «мост к NSArray». Массив [T] в Swift под капотом может оказаться не родным Swift-буфером, а «мостом» к NSArray из мира Objective-C. Поэтому компилятор вынужден на каждый доступ подстраховаться: проверить, какой это массив, и держать наготове медленный путь для мостового случая. Span устроен проще, он всегда смотрит на непрерывную «сырую» память, поэтому такой проверки ему не нужно. А что по времени? Операция p50 p90 p99 Array 75.2 82.7 94.9 Span 74.9 80.6 98.1 На нативном массиве медианы сходятся - холодная ветка с мостом в рантайме просто не исполняется. Однако Span стабильнее: в повторных прогонах Array гулял от 75 до 88 нс с тяжёлыми хвостами, а Span держал ровно ~75 нс. Итог по этому бенчмарку: для элементов-классов Span даёт не «в разы быстрее», а меньше кода, отсутствие лишней проверки на мост и более предсказуемый хвост . Для простых чисел паритет. Бенчмарк 2.2 - Span против UnsafeBufferPointer: безопасность правда бесплатна? Это, пожалуй, главный вопрос про Span . Его продают как «безопасную замену указателям без накладных расходов». Проверим на одинаковой работе - суммируем 1024 числа. Операция p50 p90 p99 Span 3.79 4.23 4.92 UnsafeBufferPointer 4.20 4.91 7.21 Array (для сравнения) 4.01 4.47 5.13 Все три в пределах шума (и всё в том же «полу» ~4 нс - компилятор одинаково развернул суммирование для всех вариантов). Это и есть заголовочный результат: на такой работе безопасность Span действительно бесплатна. Он генерирует ровно тот же машинный код, что и «сырой» небезопасный указатель, но при этом снимает с вас ответственность за границы и за время жизни буфера. И это проверяемое утверждение swiftc -O -S показывает эквивалентные горячие циклы, а не маркетинговое обещание из релиз-ноутов. Бенчмарк 2.3 - MutableSpan для правки на месте против Array с COW Рассмотрим сценарий обработки буфера - например, инверсию 4096 байт ( x = 255 − x , как при простой обработке изображения). Вариант A мутирует Array , у которого специально есть второй владелец буфера (чтобы спровоцировать COW). Вариант B правит на месте через MutableSpan . Операция p50 p90 p99 Array , правка + COW-копия 124.8 133.9 160.0 MutableSpan , правка на месте 116.2 123.0 142.2 Разница скромнее, чем в бенчмарке 1.3, - около 8–10%. Причина понятна: на 4096 байтах доминирует сам проход (4096 чтений-записей), а защитная COW-копия (одна аллокация плюс копирование 4 КБ) добавляется к нему всего один раз, а не на каждой итерации. MutableSpan эту копию убирает целиком и правит буфер на месте - стабильно быстрее и с более лёгким хвостом. Выигрыш практический, но не драматический; и он тем заметнее, чем больше мутирующих проходов вы делаете над разделяемым буфером. Почему Span нельзя вернуть из функции - и почему это фича, а не баг Самое важное про Span - то, чего он не умеет. Попробуем вернуть его из функции: func brokenSpan() -> Span { let local = [1, 2, 3] return local.span // ошибка компиляции } Span - это non-escapable тип ( ~Escapable ): его нельзя вернуть из функции, сохранить в свойство или захватить в замыкание. И это сделано намеренно. На полях: почему это спасает от «висячего указателя». Представьте, что мы всё-таки вернули span наружу. Массив local живёт только внутри функции - по выходу из неё его буфер освобождается. А span , если бы мы его вернули, продолжал бы ссылаться на уже освобождённую память. Это классический «висячий указатель» (dangling pointer) и источник трудноуловимых крашей. Swift просто запрещает создать такую ситуацию: компилятор следит, чтобы окно не жило дольше своего буфера. Это и называется lifetime dependency - «зависимость по времени жизни». Приведу вывод компилятора Swift 6.2.4 дословно - это интереснее любого пересказа. Попытка вернуть Span : error: a function cannot return a ~Escapable result 2 | func brokenSpan() -> Span { | `- error: a function cannot return a ~Escapable result Попытка сохранить Span в поле структуры: error: stored property 's' of 'Escapable'-conforming struct 'Holder' has non-Escapable type 'Span ' 8 | struct Holder { | `- note: consider adding '~Escapable' to struct 'Holder' Обратите внимание на два разных диагноза. Возврат режется формулировкой «функция не может вернуть ~Escapable -результат». А сохранение в свойство ловится иначе: «обычная структура не может содержать non-escapable поле», и компилятор даже подсказывает решение - «добавьте ~Escapable к структуре». Любой путь, которым окно могло бы пережить буфер, отсекается ещё на компиляции. И здесь важно не пообещать лишнего. Механизм @lifetime , который позволил бы вам возвращать Span из своих API, в 6.2 пока экспериментальный (полноценный proposal ещё в разработке). В стандартной библиотеке он уже работает для span-свойств, но в вашем прикладном коде - нет. Так что сегодня Span - это инструмент для параметров функций и локальных проходов, а не для возвращаемых значений. Часть 3. Строгая memory safety - белая ворона (это не про скорость) Сразу расставлю ожидания. Строгая memory safety - это диагностика компилятора , а не оптимизация в рантайме. Флаг -strict-memory-safety включает предупреждения на использование небезопасных конструкций, а атрибуты @safe / @unsafe позволяют явно разметить намерение. Поэтому здесь нет замера ns/op - мерить нечего. Мерить будем воздействие на разработчика. import Foundation // Намеренно «грязный» код с unsafe-конструкциями. func rawPointerWalk(_ p: UnsafePointer , _ n: Int) -> Int { var acc = 0 for i in 0.. Int { return a.withUnsafeBytes { raw in // unsafe closure var s = 0 for b in raw { s += Int(b) } return s } } func rebind(_ p: UnsafeRawPointer, _ n: Int) -> Int { let ip = p.assumingMemoryBound(to: Int.self) // unsafe rebind return ip[0] } func mallocWorld() -> UnsafeMutablePointer { let p = UnsafeMutablePointer .allocate(capacity: 4) // unsafe alloc p.initialize(repeating: 0, count: 4) return p } // Пример «чинки»: помечаем осознанный unsafe явно. @unsafe func documentedUnsafe(_ p: UnsafePointer ) -> Int { p[0] } Я взял файл с намеренно «грязным» кодом (несколько мест с UnsafePointer , withUnsafeBytes , assumingMemoryBound и ручным выделением памяти) и собрал его сначала обычно, потом с флагом. Сборка Предупреждений обычная ( swiftc -O ) 0 swiftc -O -strict-memory-safety 9 после починки 0 Без флага - тишина: режим строго opt-in, по умолчанию он ничего не меняет и не шумит. С флагом всплыли 9 мест, все одного вида: «выражение использует небезопасные конструкции, но не помечено как unsafe ». Ложных срабатываний на этом наборе не было - подсветилось ровно то, что действительно трогает сырые указатели. Что подсветилось и как я это чинил: Конструкция В чём небезопасность Как починил p[i] по UnsafePointer доступ по сырому указателю заменил на Span - предупреждение ушло само a.withUnsafeBytes { … } работа с сырыми байтами осознанно, пометил unsafe a.withUnsafeBytes for b in raw по сырому буферу небезопасный проход for unsafe b in unsafe raw p.assumingMemoryBound(to:) переинтерпретация типа памяти пометил unsafe UnsafeMutablePointer.allocate + initialize ручное выделение памяти пометил unsafe - это осознанное решение тело @unsafe -функции @unsafe на функции ≠ разрешение телу внутри всё равно нужен unsafe p[0] Отсюда два практических вывода, которые стоит запомнить: @unsafe на функции - не индульгенция её телу. Даже помеченная @unsafe -функция обязана внутри писать unsafe на каждом небезопасном выражении. Атрибут говорит про вызов функции («будьте осторожны, вызывая меня»), а не про её внутренности . И вторая шероховатость, самая неочевидная: for -цикл требует двойной пометки. for unsafe b in raw не гасит предупреждение до конца - само raw (небезопасного типа) тоже является unsafe-выражением, поэтому нужно for unsafe b in unsafe raw . Один абзац контекста, чтобы было ясно, зачем это вообще в языке. Строгая memory safety - часть большого разворота Swift в сторону системного программирования, туда, где раньше безраздельно царил C. И это не только внутренняя инициатива Apple: регуляторы (вплоть до рекомендаций агентств вроде CISA и NSA) всё настойчивее двигают индустрию к memory-safe языкам. Флаг даёт то, чего раньше не хватало, - способ доказуемо очертить небезопасный периметр в кодовой базе и не дать ему бесконтрольно расползаться. Это не про скорость. Это про то, чтобы «мы тут точно не трогаем ничего небезопасного» стало утверждением, которое проверяет компилятор, а не обещанием на код-ревью. Итог: когда фича реально платит Соберём всё в одну таблицу - её удобно сохранить и вернуться. Фича Где реально даёт выигрыш (по замерам) Где компилятор уже сделал это за вас Цена в эргономике InlineArray поле структуры и частое копирование по значению: ~10× (40.3 → 4.1 нс); нет COW при мутации разделяемого буфера (49.9 → 4.4 нс) локальный Array : 0 аллокаций в куче (stack promotion), создание неотличимо (4.2 ≈ 4.2 нс) нет append / remove , урезанный API, размер зашит в тип, доступность Span / MutableSpan правка на месте без COW: ~10% (124.8 → 116.2 нс); элементы-классы: вдвое меньше кода и нет пути через мост (68 → 32 инстр.) простые элементы: паритет с Array и с UnsafeBufferPointer (~4 нс, ARC и так нет) non-escapable: нельзя вернуть/сохранить; свои возвращающие API - пока экспериментально strict memory safety не про скорость: 9 небезопасных мест найдено и исправлено, 0 ложных - двойная разметка for unsafe … in unsafe … , @unsafe не освобождает тело Если собрать всё вместе, главное вот в чём. Это не «кнопки ускорения». По моим замерам все три фичи почти нигде не дают «в разы быстрее» на ровном месте - и не потому, что они плохие, а потому что оптимизатор Swift уже давно делает за вас львиную долю работы: размещает локальные массивы на стеке, убирает лишний ARC в проходах по массиву, генерирует для Span ровно тот же код, что для сырых указателей. Точечный выигрыш есть, и он реальный: InlineArray как поле структуры, MutableSpan для правки на месте, Span - меньше кода и никаких мостов для элементов-классов. Но настоящая ценность этих фич не в наносекундах. Она в том, что прежде зависело от милости оптимизатора («скорее всего, ляжет на стек», «наверное, ARC уберётся»), а теперь становится явным и проверяемым на этапе компиляции . Выигрыш в производительности - точечный. Выигрыш в предсказуемости и безопасности - системный. И, на мой взгляд, именно второе важнее первого: ради него эти инструменты и стоит брать в работу. Воспроизводимость Все числа в статье получены на: Swift 6.2.4 (swiftlang-6.2.4.1.4 clang-1700.6.4.2), Xcode 26.3 (17C529) ОС macOS 26 (Darwin 25.3.0), arm64 Чип Apple M4 Pro, 24 ГБ RAM Сборка -O (release) Полный код бенчмарков, файлы для доказательств (ассемблер, SIL, диагностики) и скрипт воспроизведения - в репозитории: https://github.com/maxches99/swift6.2_memory_bench . Повторить весь прогон можно одной командой: git clone https://github.com/maxches99/swift6.2_memory_bench ./reproduce.sh # окружение + все ns/op + анализ ассемблера + strict + ошибки компилятора И небольшая оговорка про сами числа. ns/op на уровне 4 нс - это «пол» барьера blackHole , поэтому мелкие расхождения между прогонами (±0.5 нс) нормальны и на выводы не влияют. Ключевые выводы держатся не на дрожащей последней цифре ns/op, а на ассемблере и SIL - на том, что реально сгенерировал компилятор. Это детерминированно и воспроизводимо. Если захотите перепроверить меня со статистически более строгой гарнитурой (доверительные интервалы, автоэкспорт графиков) - берите package-benchmark от Ordo One ; у меня числа сошлись с ней в пределах шума. Свой минимальный харнесс я выбрал только ради того, чтобы всё воспроизводилось одной командой и без зависимостей. Если статья была полезной - расскажите в комментариях, какие ещё фичи Swift 6.2 хотелось бы увидеть «под микроскопом» с реальными замерами.
#Ai
#Github
#Linux
#Tor
#Чат
#Git
#Скрипт
#Бот
#Erp
#Api
📊 Источник:
Habr
|
Оригинал