Развлекаемся с экспериментальными функциями swift toolchain.

Вступление

Swift - очень удобный язык, хотя и у него есть свои причуды и своеобразная нелинейность обучения. Тем не менее, с его помощью вы можете довольно быстро выпустить готовый к работе код. Как бы то ни было, иногда у вас возникают чувствительные к производительности разделы, и Swift не очень помогает сократить их количество. В таких случаях популярным выбором становится использование языка C++.

И тут возникает вопрос: "как вызвать С++ функцию через Swift"? Чтобы сделать такой вызов, зачастую вам нужно будет написать оболочку Objective-C, которая будет выступать в качестве публичного интерфейса для вашего кода на C++. В подобных случаях Swift toolchain может помочь импортировать код Objective-C в Swift. Основное ограничение этого метода заключается в том, что вы не сможете использовать классы C++ в Objective-C, только простые POD-структуры.

Мы напишем сито алгоритма Эратосфена как на C++, так и на Swift. Затем узнаем, как активировать совместимость с C++ и вызвать код C++ из Swift, а за одно сравним производительность этих реализаций. Имейте в виду, что эта режим совместимости является экспериментальной функцией и она может быть в любой момент изменена. Данная статья писалась на версии Xcode 14.2.

Алгоритм

Сито Эратосфена находит все простые числа, меньшие или равные N. Простое число — это целое число, которое делится только на себя и на 1. Алгоритм создает логический массив, чтобы определить, является ли каждое число простым. Далее он постепенно перебирает их, помечая все кратные как не простые.

Вот реализация алгоритма на Swift.

// primes.swift

func primes(n: Int) -> [Int] {
    var isPrime = [Bool](repeating: true, count: n + 1)

    for value in stride(from: 2, to: n + 1, by: 1) where isPrime[value] {
        if value * value > n { break }

        for multiple in stride(from: value * 2, to: n + 1, by: value) {
            isPrime[multiple] = false
        }
    }

    var result = [Int]()

    for value in stride(from: 2, to: n + 1, by: 1) where isPrime[value] {
        result.append(value)
    }

    return result
}

Для реализации на C++ нам нужен заголовок и исходный файл. Обратите внимание, что мы ввели typedef, чтобы иметь более чистое имя для обращения к std::vector<long>.

// primes.hpp

#include 

typedef std::vector VectorLong;

VectorLong primes(const long &n);
// primes.cpp
#include 

#include "primes.hpp"

VectorLong primes(const long &n) {
    std::vector isPrime(n + 1); // faster than std::vector
    std::fill(isPrime.begin(), isPrime.end(), true);

    for (long value = 2; value * value <= n; ++value) {
        if (!isPrime[value]) { continue; }

        for (long multiple = value * 2; multiple <= n; multiple += value) {
            isPrime[multiple] = false;
        }
    }

    VectorLong result;

    for (long value = 2; value <= n; ++value) {
        if (!isPrime[value]) { continue; }

        result.push_back(value);
    }

    return result;
}

Структура проекта

Мы создадим контейнер/package Swift с двумя отдельными targets - для хранения нашего кода Swift и C++. Чтобы импортировать код C++ из Swift, нам нужна карта модуля.

// module.modulemap

module CXX {
    header "CXX.hpp"
    requires cplusplus
}

// CXX.hpp

#include "primes.hpp"

Не забудьте передать enable-experimental-cxx-interop в Package.swift в swift targets

// swift-tools-version: 5.7

import PackageDescription

let package = Package(
    name: "SwiftCXXInteropExample",
    platforms: [
        .macOS(.v12),
    ],
    products: [
        .library(name: "CXX", targets: ["CXX"]),
        .executable(name: "CLI", targets: ["CLI"])
    ],
    dependencies: [],
    targets: [
        .target(name: "CXX"),
        .executableTarget(
            name: "CLI",
            dependencies: ["CXX"],
            swiftSettings: [.unsafeFlags(["-enable-experimental-cxx-interop"])]
        )
    ]
)

Обратитесь к документации от Apple, если вы хотите более подробно узнать о том, как включить совместимость с C++.

Тестируем результаты

Гораздо удобнее и проще использовать наш VectorLong из swift в соответствии с RandomAccessCollection.

import CXX

extension VectorLong: RandomAccessCollection {
    public var startIndex: Int { 0 }
    public var endIndex: Int { size() }
}

Теперь мы можем вызвать нашу С++ функцию из swift и вывести результат в консоль.

let cxxVector = primes(100)
let swiftArray = [Int](cxxVector)
print(swiftArray)

А теперь давайте проверим будет ли наша реализация на С++ работать быстрее.

let signposter = OSSignposter()

let count = 100
let n = 10_000_000

for _ in 0..
    let state = signposter.beginInterval("C++")
    let _ = primes(n)
    signposter.endInterval("C++", state)
}

for _ in 0..
    let state = signposter.beginInterval("Swift")
    let _ = primes(n: n)
    signposter.endInterval("Swift", state)
}

Мы видим, что производительность на С++ в среднем чуть быстрее, 26 секунд на C++ против 28 cекунд на swift.

Заключительные мысли

Мы смогли напрямую использовать std::vector в Swift. Лично я не нашел удобного способа переключения между Swift Map и std::mapSet и std::set. Тем не менее, совместимость swift c C++ быстро развивается, и будущее этой совместимости вселяет оптимизм.

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

С полным кодом можно ознакомиться по адресу https://github.com/ksemianov/SwiftCXXInteropExample

Комментарии (1)


  1. BadHandycap
    00.00.0000 00:00

    У вас код на C++ "поплыл". Написал вам еще вчера в ЛС, но вы никак не отреагировали.