Другое

Производительность Swift Beta: почему сортировка массивов медленная

Узнайте, почему сортировка массивов в Swift Beta значительно медленнее, чем в C++ и Python. Изучите уровни оптимизации, проверки безопасности и методы достижения производительности уровня C++ при сохранении функций безопасности Swift.

Производительность Swift Beta: почему сортировка массивов так медленна по сравнению с C++ и Python?

Я столкнулся со значительными проблемами производительности при сортировке массивов в Swift Beta. При реализации алгоритма я заметил, что операции сортировки значительно медленнее, чем в других языках:

Результаты тестирования

При сортировке массива из 1 000 000 целых чисел:

  • C++: 0.06 секунды
  • Python: 0.6 секунды
  • Swift с -O3: 6 секунд
  • Swift с -O0: 88 секунд

Пример кода

swift
let n = 1000000
var x = [Int](repeating: 0, count: n)
for i in 0..<n {
    x[i] = random()
}
// запустить таймер здесь
let y = sort(x)
// остановить таймер здесь

Дополнительные результаты

  1. Использование -Ofast вместо -O3 повышает производительность до уровня C++, но отключает важные проверки безопасности, такие как проверка переполнения целых чисел и проверка границ массива.

  2. Простые циклы также показывают значительные различия в производительности:

    • С -Ofast: 5 машинных инструкций в внутреннем цикле
    • С -O3: 88 строк ассемблерного кода с 26 вызовами подпрограмм во внутреннем цикле
  3. Результаты тестирования вложенных циклов:

    • C++ -O3: 0.05 с
    • Java: 0.2 с
    • Python: 12 с
    • Swift -Ofast: 0.05 с
    • Swift -O3: 23 с

Вопрос

Как можно достичь приемлемой производительности в Swift, не жертвуя языковыми средствами безопасности? Разрыв в производительности между -O3 и -Ofast указывает на то, что проверки безопасности оказывают чрезмерное влияние на производительность. Есть ли способ приблизить производительность Swift к C++, сохраняя механизмы безопасности, которые делают Swift привлекательным?

Производительность сортировки массивов в Swift Beta страдает в первую очередь из-за агрессивных проверок безопасности времени выполнения, которые включены по умолчанию на уровне оптимизации -O3, что вызывает значительный разрыв в производительности по сравнению с языками вроде C++, которые по умолчанию не обеспечивают этих механизмов безопасности. Разница в производительности между -O3 и -Ofast показывает, что эти проверки безопасности, особенно проверка границ массива и обнаружение целочисленного переполнения, оказывают чрезмерное влияние на производительность, но современные версии Swift внедрили методы оптимизации, которые могут помочь сократить этот разрыв, сохраняя гарантии безопасности.

Содержание

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

Драматическая разница в производительности, которую вы наблюдаете между Swift и C++/Python, не является inherent самой языковой реализации Swift, а скорее следствием агрессивных проверок безопасности времени выполнения, которые Swift обеспечивает по умолчанию. Согласно инженерам Apple, -O3 в Objective-C по сути эквивалентен -O в Swift, в то время как -Ofast отключает все функции безопасности.

Ключевое отличие заключается в том, что происходит на уровне ассемблера:

С -Ofast:

  • Всего 5 машинных инструкций во внутреннем цикле
  • Уровень производительности C++ (0.06 секунд для сортировки 1M целых чисел)
  • Нет проверок безопасности времени выполнения

С -O3:

  • 88 строк ассемблерного кода с 26 вызовами подпрограмм во внутреннем цикле
  • В 100 раз медленнее, чем C++ (6 секунд против 0.06 секунд)
  • Включены все проверки безопасности

Это указывает на то, что компилятор Swift в Beta генерировал чрезмерно консервативный код с избыточными проверками безопасности, которые не могли быть эффективно оптимизированы. Как отмечено в обсуждениях сообщества, -Ofast значительно меняет семантику языка — в моих тестах он отключал проверки на целочисленное переполнение и выход за границы индексации массива.


Уровни оптимизации в Swift

Swift предлагает несколько уровней оптимизации, которые балансируют производительность с безопасностью и возможностями отладки:

Обзор уровней оптимизации

Уровень Описание Производительность Функции безопасности
-Onone Без оптимизации Очень медленно (88s в вашем тесте) Полные проверки безопасности
-O Стандартная оптимизация Хорошая Полные проверки безопасности
-Osize Оптимизация размера Умеренная Полные проверки безопасности
-O3 Агрессивная оптимизация Лучшая с безопасностью Полные проверки безопасности
-Ofast Самый быстрый, но небезопасный Уровень производительности C++ Отключенные проверки безопасности

Флаг SWIFT_DISABLE_SAFETY_CHECKS (найденный в настройках оптимизации Xcode) можно использовать для отключения проверок безопасности времени выполнения при включенной оптимизации, по сути предоставляя поведение -Ounchecked.

Согласно документации по современному Swift, эти проверки могут быть неуместны в высокопроизводительном коде, если вы либо знаете, что переполнение невозможно, либо результатом позволения операции обернуться (wrap around) является правильный.


Проверки безопасности и их влияние на производительность

Проверки безопасности Swift включают несколько механизмов, влияющих на производительность:

Проверка границ массива

Swift гарантирует, что доступ к элементам массива всегда находится в пределах границ. Как упоминается в сессии WWDC 2015 о производительности Swift, Swift может оптимизировать это путем “выноса проверок за пределы циклов”. Однако в версии Beta эта оптимизация могла быть не такой эффективной.

Проверка целочисленного переполнения

Swift включает встроенную защиту от целочисленного переполнения, что добавляет проверки времени выполнения к арифметическим операциям. Эти проверки могут быть особенно дорогими в тесных циклах.

Разрыв в производительности, который вы наблюдали, указывает на то, что в Swift Beta эти проверки выполнялись на каждой итерации, а не оптимизировались или выносились за пределы циклов. Как отмечено в одном анализе, компилятор Swift может “вынести эту проверку за пределы цикла, сделав стоимость проверки незначительной”, но эта оптимизация могла быть не полностью реализована в версии Beta.

ARC (Автоматический подсчет ссылок)

Обсуждение на Reddit указывает, что “ARC не правильно оптимизирует ненужные retain/release” в версии Beta, что повлияет на производительность любого кода, связанного с объектными ссылками.


Современные методы оптимизации

Современные версии Swift внедрили несколько методов для повышения производительности при сохранении безопасности:

Устранение проверок границ

Swift улучшил свою способность устранять избыточные проверки границ. Компилятор может анализировать циклы и определять, когда проверки границ не нужны, удаляя их из внутреннего цикла. Согласно документации по оптимизации Swift, “Swift может выносить проверки за пределы циклов. Таким образом, проверки O(n) становятся O(1).”

Оптимизация целочисленного переполнения

Для целочисленных операций Swift часто может доказать на этапе компиляции, что переполнение невозможно, позволяя устранить проверки времени выполнения. То же самое относится к преобразованиям типов, таким как FixedWidthInteger.init(truncatingIfNeeded:).

Оптимизация статической диспетчеризации

Swift поддерживает как динамическую, так и статическую диспетчеризацию. Как отмечено в руководствах по оптимизации производительности, “статическая диспетчеризация разрешается на этапе компиляции, что приводит к более быстрым вызовам методов”. Используя ключевые слова final и private, вы можете помочь компилятору более эффективно использовать статическую диспетчеризацию.

Оптимизация компоновки памяти

Современный Swift улучшил оптимизацию компоновки памяти, особенно для типов значений, таких как массивы. Использование withUnsafeBufferPointer может обеспечить прямой доступ к памяти при сохранении безопасности.


Практические рекомендации по повышению производительности

Для вашего текущего кода Swift Beta

  1. Используйте -Ofast для критически важного кода с пониманием того, что вы отключаете проверки безопасности:

    bash
    swiftc -Ofast ваш_файл.swift
    
  2. Вручную проверяйте условия безопасности при использовании -Ofast:

    • Добавляйте утверждения (assertions) для границ массива
    • Проверяйте возможное целочисленное переполнение
    • Проверяйте входные данные
  3. Рассмотрите алгоритмические оптимизации:

    • Используйте более эффективные алгоритмы сортировки, когда это возможно
    • Минимизируйте выделение памяти в тесных циклах
    • Рассмотрите использование ContiguousArray для лучшей компоновки памяти

Лучшие практики для современного Swift

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

  1. Используйте правильные уровни оптимизации:

    swift
    // В Xcode установите уровень оптимизации "Fastest, Optimized" (-O)
    // Или "Fastest, Unchecked" (-Ounchecked) для максимальной производительности
    
  2. Включите оптимизацию всего модуля:

    swift
    // В настройках сборки: Включите Whole Module Optimization
    
  3. Используйте аннотации для критически важного кода:

    swift
    @inlinable
    @usableFromInline
    func оптимизированнаяФункция() { ... }
    
  4. Рассмотрите небезопасные альтернативы с соответствующими мерами предосторожности:

    swift
    // Используйте withUnsafeBufferPointer для прямого доступа к памяти
    x.withUnsafeBufferPointer { buffer in
        // Прямые операции с памятью
    }
    
  5. Профилируйте и оптимизируйте горячие пути:

    • Используйте Instruments для определения узких мест
    • Сосредоточьтесь на оптимизации критически важных разделов
    • Стратегически используйте атрибут @inline

Путь миграции к лучшей производительности

Если вы работаете с Swift Beta и вам нужна лучшая производительность, вот стратегия миграции:

Краткосрочные решения

  1. Используйте -Ofast с ручными проверками безопасности для немедленного повышения производительности
  2. Профилируйте ваш код для определения основных узких мест
  3. Рассмотрите улучшения алгоритмов, которые не зависят от оптимизаций компилятора

Долгосрочные решения

  1. Обновитесь до современной версии Swift, где эти оптимизации были улучшены

  2. Реализуйте проверки безопасности на более высоком уровне, а не полагайтесь на проверки времени выполнения

  3. Рассмотрите использование межъязыковой совместимости с C++ для критически важных компонентов производительности:

    swift
    // Импортируйте функции C++ для критически важных операций
    import Cxx
    
  4. Используйте инструменты тестирования производительности Swift для проверки улучшений:

    swift
    import XCTest
    func тестПроизводительности() {
        measure {
            // Ваш критически важный код
        }
    }
    

Разрыв в производительности, который вы испытываете в Swift Beta, в значительной степени был решен в более поздних версиях Swift благодаря улучшенным оптимизациям компилятора и более агрессивному устранению проверок границ. Современный Swift может достичь производительности, гораздо более близкой к C++, сохраняя большинство гарантий безопасности, особенно при использовании правильных методов оптимизации и современных возможностей языка.

Источники

  1. Swift Beta performance: sorting arrays - Stack Overflow
  2. Swift Beta performance: sorting arrays - F-Secure Community
  3. Apples to apples · Jesse Squires
  4. Optimization levels in Xcode - Stack Overflow
  5. How Swift Array Bounds Checks Optimization works - Stack Overflow
  6. Swift/docs/OptimizationTips.rst at main · swiftlang/swift
  7. Low-level Swift optimization tips - Swiftinit
  8. Optimizing Swift Performance - Medium
  9. Compile-time code optimization for Swift and Objective-C - topolog’s tech blog
  10. Insider Tips for Swift Performance Optimization - MoldStud

Заключение

Проблемы с производительностью сортировки массивов в Swift Beta обусловлены агрессивными проверками безопасности времени выполнения, которые не могли быть эффективно оптимизированы в компиляторе. Драматический разрыв в производительности между -O3 и -Ofast показывает, что эти механизмы безопасности оказывали чрезмерное влияние на производительность.

Для достижения разумной производительности в Swift без компромиссов в безопасности:

  1. Современные версии Swift значительно улучшили методы оптимизации, включая лучшее устранение проверок границ и оптимизацию целочисленного переполнения
  2. Используйте соответствующие уровни оптимизации - -O для большинства случаев, -Ounchecked для критически важных разделов с ручной проверкой безопасности
  3. Применяйте аннотации для критически важного кода, такие как @inlinable и @usableFromInline, чтобы помочь оптимизатору
  4. Рассмотрите алгоритмические оптимизации, которые уменьшают необходимость в проверках времени выполнения
  5. Профилируйте ваш код, чтобы определить и оптимизировать фактические узкие места, а не гадать

Язык Swift значительно эволюционировал со времен версий Beta, и современный Swift может достичь производительности, гораздо более близкой к C++, сохраняя большинство гарантий безопасности. Для достижения наилучших результатов рассмотрите возможность обновления до последней версии Swift и применения методов оптимизации, упомянутых в этом руководстве.

Авторы
Проверено модерацией
Модерация