Игра с экспериментальным взаимодействием Swift-C++

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

И возникает вопрос «как мне назвать этот C++ func из Swift»?

Обычно вам нужно написать оболочку Objective-C, которая будет действовать как общедоступный интерфейс для вашего кода на C++. Инструментальная цепочка Swift может импортировать объявления 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 <vector>
typedef std::vector<long> VectorLong;
VectorLong primes(const long &n);
// primes.cpp
#include <algorithm>

#include "primes.hpp"
VectorLong primes(const long &n) {
    std::vector<char> isPrime(n + 1); // faster than std::vector<bool>
    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;
}

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

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

// module.modulemap

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

// CXX.hpp

#include "primes.hpp"

И не забудьте передать -enable-experimental-cxx-interop цели Swift в Package.swift.

// 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() }
}

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

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

Давайте посмотрим, действительно ли наша реализация на C++ работает быстрее.

let signposter = OSSignposter()

let count = 100
let n = 10_000_000

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

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

Чуть быстрее со средней продолжительностью 26 мс против 28 мс для Swift.

Последние мысли

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

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

Полный код смотрите здесь.

Первоначально опубликовано на https://ksemianov.github.io/articles/cpp-interop/