8,1
рейтинг
18 декабря 2015 в 18:45

Разработка → Сравниваем Swift и Rust

Swift*, Rust*

Поводом для написания статьи стала публикация исходного кода языка Swift — мне стало интересно поближе познакомиться с ним. В глаза сразу же бросилась схожесть синтаксиса с другим молодым языком программирования под названием Rust, к тому же за схожими синтаксическими конструкциями просматривалась и схожая область применения этих языков. Оба языка имеют сильную статическую типизацию с локальным выводом типов, оба компилируются напрямую в машинный код. И тот и другой языки впитали в себя многие приемы из мира функционального программирования. И Swift и Rust имеют средства для запуска кода, написанного на C, что позволяет без труда писать обертки над огромным количеством библиотек. Оба языка рассматриваются как замена существующим системным языкам типа C, C++, ObjectiveC. Так что же в них общего, а что различается?


Основы синтаксиса



Сразу оговорюсь, что я не пытаюсь рассказать читателям основы сразу двух языков программирования, если что-то неясно, то рекомендую обратиться за помощью к Rustbook или Swiftbook.

Для начала давайте попробуем сравнить простейшие программы:

Rust
fn main() {
    let i:i32 = 16;
    let mut f = 8.0;
    f *= 2.0;

    println!("Hello Rust! within vars {} and {}", i, f);
}


Swift
let i:Int = 10
var f = 15.0
f /= 2

print("Hello Swift within vars \(i) \(f)")


Можно заметить, что let и там и там означает одно и тоже, а ключевое слово var в Swift аналогично комбинации let mut в Rust. Но имеются и различия: в Rust во всех числовых типах однозначно указывается размер, а Swift же следует традициям Си. В этом вопросе Rust выглядит более низкоуровневым.
Интерполяция строк в Rust'е делается при помощи макроса format!, который вызывается в недрах println!, а в Swift'е же это фича самого языка, но при этом я не нашел в документации способов задать опции форматирования.
Интересна разница в трактовании ";": для Rust'а это признак окончания выражения, что позволяет делать некоторые изящные вещи, например последнее выражение внутри функции автоматически становится возвращаемым значением. Swift же просто следует традициям pascal'я, где точка с запятой просто разделяет операторы, находящиеся на одной строке.

Теперь давайте попробуем создать функцию, которая принимает функцию от двух аргументов и преобразует ее в функцию от одного:

Rust
fn apply<F: 'static>(f: F, v1: i32) -> Box<Fn(i32) -> ()>
    where F: Fn(i32, i32) -> ()
{
    Box::new(move |v2| f(v1, v2))
}

fn print_sum(a: i32, b: i32) -> ()
{
    println!("Rust: sum a and b is {}", a + b);
}

fn main() {
    let a = 2; let b = 5;
    print_sum(a, b);

    let f = print_sum;
    let f2 = apply(f, b);
    f2(a);
}


swift
func apply(f: (_: Int, _: Int) -> (), _ v1: Int) -> (_: Int) -> ()
{
    return {(c: Int) -> () in
        return f(v1, c)
    }
}

func print_sum(a: Int, second b: Int) -> ()
{
    print("Swift: sum a and b is \(a+b)")
}

let a = 2; let b = 5;
print_sum(a, second:b)

let f2 = apply(print_sum, b)
f2(a)


Здесь уже явно заметны различия, я оставлю за бортом чисто синтаксические отличия типа внешних имен у именованных параметров в Swift или обобщений в Rust, и перейду к рассмотрению более существенной разницы. Подход Swift заметно более высокоуровневый: компилятор сам решает каким образом хранить нашу результирующую функцию, для Rust же пришлось её явно упаковывать в box, потому, что moved замыкания являются безразмерным типом.
Давайте проверим, чьи же лямбда функции работают быстрее:

Код бенчмарков
Rust
fn apply<F: 'static>(f: F, v1: i32) -> Box<Fn(i32) -> i32>
    where F: Fn(i32, i32) -> i32
{
    Box::new(move |v2| f(v1, v2))
}

fn make_sum(a: i32, b: i32) -> i32
{
    a + b
}

fn main() {
    let a = 2; let b = 5;
    let c = make_sum(a, b);
    println!("Rust: c is {}", c);

    let f2 = apply(make_sum, b);

    let mut d = 0;
    for i in 0..1000000000 {
        d = f2(i);
    }

    println!("Rust: d is {}", d);
}



swift
func apply(f: (_: Int, _: Int) -> Int, _ v1: Int) -> (_: Int) -> Int
{
    return {(c: Int) -> Int in
        return f(v1, c)
    }
}

func make_sum(a: Int, second b: Int) -> Int
{
    return a + b
}

let a = 2; let b = 5;
let c = make_sum(a, second:b)
print("Swift: c is \(c)")

let f2 = apply(make_sum, b)
f2(a)

var d = 0;
for i in 0...1000000000 {
    d = f2(i);
}

print("Swift: d is \(d)");




Итоговый результат: 4.0 секунды у Rust против 1.17 у Swift. Получается, что в случае более абстрактного кода у компилятора появляется больше возможностей для оптимизации, но на канале ruRust/general мне подсказали способ, который может и не выглядит так красиво, но зато позволяет оптимизатору выложится по полной. В конечном счете Rust посчитал весь цикл прямо во время компиляции, что очень круто.
В умелых руках Rust позволяет творить чудеса.

Код быстрой версии
struct Curry<'a> {
    f: &'a Fn(i32, i32) -> i32,
    v1: i32
}

impl<'a> Curry<'a> {
    fn new<F: 'static>(f: &'a F, v1: i32) -> Curry<'a> where F: Fn(i32, i32) -> i32  {
        Curry { f: f, v1: v1 }
    }

    fn call(&'a self, v2: i32) -> i32 {
        (*self.f)(self.v1, v2)
    }
}

fn make_sum(a: i32, b: i32) -> i32
{
    a + b
}

fn main() {
    let a = 2; let b = 5;
    let c = make_sum(a, b);
    println!("Rust: c is {}", c);

    let borrow = &make_sum;
    let f2 = Curry::new(borrow, b);

    let mut d = 0;
    for i in 0..1000000000 {
        d = f2.call(i);
    }

    println!("Rust: d is {}", d);
}



Перечисления и сопоставление с образцом:



Оба языка имеют большие возможности для сопоставления с образцом, в обоих языках существуют алгебраические типы данных. Давайте попробуем написать простенький пример, использующий эти возможности: попробуем реализовать математические операции. Для этого нам нужно будет сделать наши перечисления рекурсивными. В качестве бонуса попробуем распечатать на экран имена наших операций, используя протоколы(типажи).
Rust
enum MathOperation {
    Value(i32),
    Sum(Box<MathOperation>, Box<MathOperation>),
    Mul(Box<MathOperation>, Box<MathOperation>)
}

trait HasName {
    fn name(&self) -> &'static str;
}

impl HasName for MathOperation {
    fn name(&self) -> &'static str {
        match *self {
            MathOperation::Value(..) => "Value",
            MathOperation::Sum(..) => "Sum",
            MathOperation::Mul(..) => "Mul"
        }
    }
}

impl MathOperation {
    fn solve(&self) -> i32 {
        match *self {
            MathOperation::Value(i)         => i,
            MathOperation::Sum(ref left, ref right) => left.solve() + right.solve(),
            MathOperation::Mul(ref left, ref right) => left.solve() * right.solve()
        }
    }
}

fn main() {
    let op = MathOperation::Sum(Box::new(MathOperation::Value(10)),
                                Box::new(MathOperation::Mul(Box::new(MathOperation::Value(20)),
                                                            Box::new(MathOperation::Value(2)))));
                                
    ;
    println!("Rust: op is {} solved {}", op.name(), op.solve());
}



Swift
enum MathOperation {
  case Value(Int)
  indirect case Sum(MathOperation, MathOperation)
  indirect case Mul(MathOperation, MathOperation)
  
  func solve() -> Int {
    switch self {
    case .Value(let value):
        return value
    case .Sum(let left, let right):
        return left.solve() + right.solve()
    case .Mul(let left, let right):
        return left.solve() * right.solve()
    }
  }
}

protocol HasName {
  func name() -> String;
}

extension MathOperation : HasName
{
  func name() -> String {
    switch self {
      case .Value(_):
        return "Value"
      case .Sum(_, _):
        return "Sum"
      case .Mul(_, _):
        return "Mul"
      }
  }
}

let op = MathOperation.Sum(MathOperation.Value(10),
                           MathOperation.Mul(MathOperation.Value(20),
                                             MathOperation.Value(2)))
                                             
print("Swift: op is \(op.name()) solved \(op.solve())");



Мы можем заметить, что в Rust для организации рекурсивных перечислений потребовалось упаковать их в контейнер Box, потому, что их размер может быть произвольным и мы не можем знать на этапе компиляции сколько нам для этого потребуется памяти. В Swift для обозначения рекурсивных перечислений используется слово indirect, но учитывая его модель памяти возникает вопрос, разве компилятор сам не в состоянии разобраться с тем как нужно выделать память? По-видимому это ключевое слово введено скорее для человека.
Также мы можем видеть, что impl и extension в принципе выполняют примерно похожую работу, а типажи похожи на протоколы. Но в Swift подход более компромиссный: не обязательно добавлять методы как расширения, их можно указать прямо в объявлении перечисления.

Теперь давайте просто посмотрим на пару примеров сопоставления с образцом:

Rust (примеры взяты из Rust by example)

match some_value {
    Ok(value) => println!("got a value: {}", value),
    Err(_) => println!("an error occurred"),
}

enum OptionalTuple {
    Value(i32, i32, i32),
    Missing,
}

let x = OptionalTuple::Value(5, -2, 3);

match x {
    OptionalTuple::Value(..) => println!("Got a tuple!"),
    OptionalTuple::Missing => println!("No such luck."),
}

let x = 1;

match x {
    1 ... 5 => println!("one through five"),
    _ => println!("anything"),
}

let x = 1;

match x {
    e @ 1 ... 5 => println!("got a range element {}", e),
    _ => println!("anything"),
}

Swift (код примеров взят из Swiftbook)
let count = 3000000000000
let countedThings = "stars in the Milky Way"
var naturalCount: String
switch count {
case 0:
    naturalCount = "no"
case 1...3:
    naturalCount = "a few"
case 4...9:
    naturalCount = "several"
case 10...99:
    naturalCount = "tens of"
case 100...999:
    naturalCount = "hundreds of"
case 1000...999999:
    naturalCount = "thousands of"
default:
    naturalCount = "millions and millions of"
}
print("There are \(naturalCount) \(countedThings).")
//  выведет "There are millions and millions of stars in the Milky Way."

let somePoint = (1, 1)
switch somePoint {
case (0, 0):
    print("(0, 0) is at the origin")
case (_, 0):
    print("(\(somePoint.0), 0) is on the x-axis")
case (0, _):
    print("(0, \(somePoint.1)) is on the y-axis")
case (-2...2, -2...2):
    print("(\(somePoint.0), \(somePoint.1)) is inside the box")
default:
    print("(\(somePoint.0), \(somePoint.1)) is outside of the box")
}// выведет "(1, 1) is inside the box

let anotherPoint = (2, 0)
switch anotherPoint {
case (let x, 0):
    print("on the x-axis with an x value of \(x)")
case (0, let y):
    print("on the y-axis with a y value of \(y)")
case let (x, y):
    print("somewhere else at (\(x), \(y))")
}
// выведет "on the x-axis with an x value of 2


Здесь в целом всё очень похоже. В сопоставлении не должно быть провалов, можно указывать диапазоны значений, есть возможность привязки значений, можно использовать кортежи в сопоставлении. Хотя Rust также позволяет использовать и целые структуры при сопоставлении. Можно задавать дополнительные условия через if и where, соответственно. Но при этом в Swift есть ещё дополнительные операторы управления потоком. Хотя я и не уверен, что их использование — это хорошая идея.

Более реальный пример



Давайте попробуем написать алгоритм растеризации Брезенхема. Только не в обычной форме, а в форме для nмерных векторов и без использования вещественных чисел. Это всё может здорово пригодится при изучении краткого курса компьютерной графики.

Для начала просто попробуем создать 3х мерный вектор и определить для него операции взятия индекса и сравнения:
Rust
#[derive(Copy, Clone, PartialEq)]
struct Vec3 {
    x: i32,
    y: i32,
    z: i32
}

impl Index<usize> for Vec3
{
    type Output = i32;

    fn index<'a>(&'a self, i: usize) -> &'a Self::Output {
        match i {
            0   => &self.x,
            1   => &self.y,
            2   => &self.z,
            _   => panic!("Wrong index"),
        }
    }
}
impl IndexMut<usize> for Vec3
{
    fn index_mut<'a>(&'a mut self, i: usize) -> &'a mut Self::Output {
        match i {
            0   => &mut self.x,
            1   => &mut self.y,
            2   => &mut self.z,
            _   => panic!("Wrong index"),
        }
    }    
}

swift
struct Vector3 {
    var x: Int;
    var y: Int;
    var z: Int;

    subscript(i: Int) -> Int {
        get {
            precondition(i >= 0 && i < 3, "Index out-of-bounds")
            switch i {
            case 0: return self.x
            case 1: return self.y
            case 2: return self.z
            default: return 0
            }
        }
        set {
            precondition(i >= 0 && i < 3, "Index out-of-bounds")
            switch i {
            case 0: self.x = newValue
            case 1: self.y = newValue
            case 2: self.z = newValue
            default: break
            }
        }        
    }
}

func == (left: Vector3, right: Vector3) -> Bool {
    return (left.x == right.x) && (left.y == right.y) && (left.z == right.z)
}
func != (left: Vector3, right: Vector3) -> Bool {
    return !(left == right)
}

В Rust'е на самом деле операторы реализуются через типажи, если структура реализует типаж Index, то для нее можно применять оператор [], в Swift'е операторы взятия индекса записываются с помощью ключевого слова subscript и позволяют добавлять специальные предусловия. Rust же полагается на обычные ассерты. В случае же с другими операторами Swift позволяет как переопределять существующие, так и определять свои собственные, причем с указанием приоритетов, ассоциативности. Всё это здорово может помочь при написании математических библиотек, сделав код более похожим на исходные математические выражения. Rust же умеет автоматически создавать реализации некоторых типажей через атрибут derive, что позволяет не писать код самому во многих тривиальных случаях.

Давайте теперь попробуем создать удобный буфер для хранения изображения так, чтобы к цвету любого пикселя можно было обращаться через операторы индекса. При этом выдвинем требование, чтобы пиксельная карта умела различную глубину цвета, поэтому будем делать её обобщенной:

rust
struct GenericPixmap<T> {
    w: usize,
    h: usize,
    
    data: Vec<T> 
}

impl<T> GenericPixmap<T> 
    where T: Copy + Clone 
{
    fn new(w: usize, h: usize, fill_value: T) -> GenericPixmap<T> {
        GenericPixmap {
            w: w,
            h: h,
            data: vec![fill_value; w*h]
        }
    }
}

impl<T> Index<usize> for  GenericPixmap<T> 
    where T: Copy + Clone 
{
    type Output = [T];

    fn index<'a>(&'a self, i: usize) -> &'a Self::Output {
        let from = i*self.w;
        let to = from+self.w;
        &self.data[from..to]
    }
}
impl<T> IndexMut<usize> for GenericPixmap<T> 
    where T: Copy + Clone 
{
    fn index_mut<'a>(&'a mut self, i: usize) -> &'a mut Self::Output {
        let from = i*self.w;
        let to = from+self.w;
        &mut self.data[from..to]
    }    
}

type Pixmap = GenericPixmap<u32>;


swift
struct GenericPixmap<T> {
    let w: Int
    let h: Int

    var data: [T]

    init(width: Int, height: Int, fillValue: T) {
        self.w = width
        self.h = height
        self.data = [T](count: w*h, repeatedValue: fillValue)
    }

    func indexIsValid(x: Int, _ y: Int) -> Bool {
        return x >= 0 && x < w && y >= 0 && y < h
    }

    subscript(x: Int, y: Int) -> T {
        get {
            precondition(indexIsValid(x, y), "Index out-of-bounds")
            return data[x * y + y]
        }
        set {
            precondition(indexIsValid(x,y), "Index out-of-bounds")
            data[x * y + y] = newValue
        }
    }
}

typealias Pixmap = GenericPixmap<UInt32>


Правила для обобщений в Rust'е более строгие и нам нужно явно указывать, что шаблонный тип должен уметь копироваться и создавать свою копию. В Swift'е же для полей структуры можно явно задавать их изменяемость. А ещё можно заметить ключевое слово init. Это конструктор класса или структуры, их может быть несколько, они могут друг другу делегировать свои полномочия. В результате это выливается в достаточно сложный и многоступенчатый процесс, который, тем не менее, точно гарантирует, что каждый член будет проинициализирован. В Rust'е же есть почленная инициализация и соглашение на то, что объект должен создаваться статической функцией new. Если же процесс обещает быть сложным, то рекомендуется использовать фабрики. Что же касается статических функий, то синтаксис Rust'а в этом смысле следует традициям python, а Swift же C++.
Хочу заметить, что оператор индекса в Swift может принимать любое количество аргументов любых типов, поэтому там можно написать оператор, получающий сразу конкретный элемент массива, в Rust'е же нужно создавать срез.

Теперь давайте создадим типаж Canvas, который позволяет нам рисовать не задумываясь о реализации самого процесса.
rust
trait Canvas
{
    fn set_pixel(&mut self, x: usize, y:usize, color:u32);
}

impl Canvas for Pixmap
{
    fn set_pixel(&mut self, x: usize, y:usize, color:u32)
    {
        self[x][y] = color;
    }
}

swift
protocol Canvas {
    func setPixel(x: Int, _ y: Int, color: UInt32);
}

class MyCanvas : Canvas {
    var pixmap: Pixmap
    
    init(width: Int, height: Int, fillValue: UInt32) {
        self.pixmap = Pixmap(width:width, height:height, fillValue:fillValue)
    }
    
    func setPixel(x: Int, _ y: Int, color: UInt32)
    {
        pixmap[x, y] = color
    }
}


К сожалению, не смог быстро разобраться с тем, как реализовать расширение для типа GenericPixmap, поэтому решил создать новый класс MyCanvas, который бы реализовывал протокол Canvas, в отличии от Rust'а в Swift'е можно наследоваться от протоколов и не только.

Теперь мы подошли к самому интересному — реализации алгоритма Брезенхема. Мы хотим нарисовать линию из точки (x1, y1, z1) в точку (x2, y2, z2), для этого нам нужно сделать (|x2-x1|, |y2-y1|, |z2-z1|) шагов в направлении, которое зависит от знаков выражения (x2-x1, y2-y1, z2-z1).
Итак, нам нужно пройти (rx, ry, rz) шагов в направлениях (sx, sy, sz), для этого мы находим ось, вдоль которой нужно совершить наибольшее число шагов. Перемещение на каждом шаге будет равно (rx/r[max, ry/r[max], rz/r[max]), при этом шаг будет происходить только если суммарное перемещение d стало больше единицы, тогда по оси делается шаг, а из суммарного перемещения вычитается единица. То есть:
d[i] += r[i] / r[max]
if d[i] >= 1 { r[i] -= s[i]; d[i] -= 1; }

Несложно заметить, что если домножить условие на rmax, то можно вообще обойтись без операции деления.
d[i] += r[i]
if d[i] >= r[max] { r[i] -= s[i]; d[i] -= r[max]; }

В итоге этот вариант алгоритма работает для любого числа координат и избегает операций с плавающей точкой, которые почти всегда затратнее операций с целыми числами.

Давайте попробуем написать генератор, который создает последовательность точек между конечными узлами a и b. При этом точка a будет включаться в последовательность.

rust
struct RasterState {
    step: Vec3,
    d: Vec3,
    major_axis: usize,
}

struct LineRasterizer {
    from: Vec3,
    to: Vec3,

    state: Option<RasterState>
}

impl LineRasterizer {

    fn new(from: Vec3, to: Vec3) -> LineRasterizer {
        LineRasterizer {
            from: from,
            to: to,
            state: None
        }
    }

    fn next_point(&mut self) -> Option<Vec3> {
        match self.state {
            None => {
                let mut state = RasterState {
                    step: Vec3 { x: 0, y: 0, z: 0 },
                    d: Vec3 { x: 0, y: 0, z: 0 },
                    major_axis: 0
                };   

                let mut max = 0;
                for i in 0..3 {
                    let d = self.to[i] - self.from[i];
                    state.step[i] = if d > 0 { 1 } else { -1 };
                    
                    let d = d.abs();
                    if d > max {
                        max = d;
                        state.major_axis = i as usize;
                    };
                }

                self.state = Some(state);
                Some(self.from)
            },
            Some(ref mut state) => {
                if self.from == self.to {
                    None
                } else {
                    let from = self.from; let to = self.to;
                    let calc_residual_steps = |axis| { (to[axis] - from[axis]).abs() };
                    
                    self.from[state.major_axis] += state.step[state.major_axis];                    
                    let rs_base = calc_residual_steps(state.major_axis);
                    for i in 0..3 {
                        let rs = calc_residual_steps(i);
                        
                        if rs > 0 && i != state.major_axis {
                            state.d[i] += rs;
                            if state.d[i] >= rs_base {
                                state.d[i] -= rs_base;
                                self.from[i] += state.step[i];
                            }
                        }
                    }                    

                    Some(self.from)
                }
            },
        }
    }
}

swift
class LineRaster {

    class State {
        var step: Vector3
        var d: Vector3
        var majorAxis: Int

        init() {
            self.step = Vector3(x: 0, y: 0, z: 0)
            self.d = Vector3(x: 0, y: 0, z: 0)
            self.majorAxis = 0
        }
    }

    var from: Vector3
    let to: Vector3
    var state: State?

    init(from: Vector3, to: Vector3) {
        self.from = from
        self.to = to
    }

    func next_point() -> Vector3? {
        if let state = self.state {
            if (self.from == self.to) {
                return nil
            } else {
                let calsResidualSteps = {axis in return abs(self.to[axis] - self.from[axis])}
                
                self.from[state.majorAxis] += state.step[state.majorAxis];
                let rsBase = calsResidualSteps(state.majorAxis);
                for i in 0..<3 {
                    let rs = calsResidualSteps(i);
                    
                    if rs > 0 && i != state.majorAxis {
                        state.d[i] += rs;
                        if state.d[i] >= rsBase {
                            state.d[i] -= rsBase;
                            self.from[i] += state.step[i];
                        }
                    }
                }
                
                return self.from
            }
        } else {
            let state = State()
            var max = 0;
            for i in 0..<3 {
                let d = self.to[i] - self.from[i];
                state.step[i] = d > 0 ? 1 : -1;
                
                let da = abs(d);
                if da > max {
                    max = da;
                    state.majorAxis = i;
                };                
            }
            self.state = state
            return self.from
        }
    }
}


Состояние генератора я решил сделать в виде опционального значения, это позволяет нам легко и сразу вернуть исходную точку from из генератора без необходимости заведения дополнительных флагов. В Rust опциональные значения сделаны просто через enum Option, в то время, как в Swift'е они являются частью языка, что позволяет легко описывать опциональные цепочки вызовов без лишнего синтаксического шума.
В Rust'е используется продвинутая система владения, чтобы подсказать ей, что мы одалживаем State из перечисления по ссылке, нужно писать ключевое слово ref. В Swift'е же State по умолчанию ссылочный тип данных, а move семантики в языке пока не наблюдается, поэтому мы можем просто взять и распаковать state ни о чем не заботясь.

Писать код типа:
while let Some(point) = rasterizer.next_point() { ... }

Мне кажется не слишком изящным, гораздо логичнее для этого выглядит.
for point in generator { ... }

К счастью для того, чтобы можно было использовать цикл for достаточно просто реализовать несколько типажей для нашего генератора.
rust
impl Iterator for LineRasterizer
{
    type Item = Vec3;

    fn next(&mut self) -> Option<Self::Item> {
        self.next_point()
    }
}

swift
extension LineRaster : GeneratorType {
    func next() -> Vector3? {
        return self.next_point()
    }
}

extension LineRaster : SequenceType {
    func generate() -> LineRaster {
        return self
    }
}


Причем для Swift'а нужно реализовать целых два протокола, в то время, как для Rust'а достаточно одного, но никакой принципиальной разницы это не несёт.

Давайте немного померяемся производительностью



Скорость исполнения кода является одним из важных факторов при сравнении различных языков программирования и глупо было бы написать статью не проведя замеры производительности.

Наивный вариант сравнения:

rust
fn test_code(canvas: &mut Canvas) {
    let a = Vec3 { x: 0, y:0, z:0 };
    let b = Vec3 { x: 50, y: 55, z: -20 };

    let rasterizer = LineRasterizer::new(a, b);
    for point in rasterizer {
        let color = std::u32::MAX;
        canvas.set_pixel(point.x as usize, point.y as usize, color);
    }
}

for _ in 0..1000000 {
    test_code(&mut canvas)
}

swift
func testCode(canvas: Canvas) -> () {
    let a = Vector3(x: 0, y:0, z:0)
    let b = Vector3(x: 50, y:55, z:-20)
    let raster = LineRaster(from: a, to: b)
    for point in raster {
        let color = UInt32.max
        canvas.setPixel(point.x, point.y, color: color)
    }
}

...

var myCanvas: Canvas = canvas
for _ in 0..<1000000 {
    testCode(myCanvas)
}


Мы просто передадим ссылку на Canvas в тестируемую функцию и замеряем время.
Получилось 0.86 у Rust против 5.3 у Swift. Вполне вероятно, что Rust как-то заинлайнил вызовы, а Swift же остался на уровне динамической диспетчеризации. Чтобы проверить это попробуем написать обобщенную функцию.
rust
fn test_code_generic<T: Canvas>(canvas: &mut T) {
    let a = Vec3 { x: 0, y:0, z:0 };
    let b = Vec3 { x: 50, y: 55, z: -20 };

    let rasterizer = LineRasterizer::new(a, b);
    for point in rasterizer {
        let color = std::u32::MAX;
        canvas.set_pixel(point.x as usize, point.y as usize, color);
    }
}

swift
func testCodeGeneric<T:Canvas>(canvas: T) -> () {
    let a = Vector3(x: 0, y:0, z:0)
    let b = Vector3(x: 50, y:55, z:-20)
    let raster = LineRaster(from: a, to: b)
    for point in raster {
        let color = UInt32.max
        canvas.setPixel(point.x, point.y, color: color)
    }
}

Результаты 0.83 у Rust, против 4.94 у Swift, что говорит нам о том, что всё-таки Swift сумел лучше соптимизировать код, но где-то ещё остались узкие места, в которых он не смог разобраться.

Теперь попробуем упаковать Canvas в box, а для Swift воспользоваться модификатором inout, который по своему действию подобен &mut.
rust
fn test_code_boxed(canvas: &mut Box<Canvas>) {
    let a = Vec3 { x: 0, y:0, z:0 };
    let b = Vec3 { x: 50, y: 55, z: -20 };

    let rasterizer = LineRasterizer::new(a, b);
    for point in rasterizer {
        let color = std::u32::MAX;
        canvas.set_pixel(point.x as usize, point.y as usize, color);
    }
}

swift
func testCodeInout(inout canvas: Canvas) -> () {
    let a = Vector3(x: 0, y:0, z:0)
    let b = Vector3(x: 50, y:55, z:-20)
    let raster = LineRaster(from: a, to: b)
    for point in raster {
        let color = UInt32.max
        canvas.setPixel(point.x, point.y, color: color)
    }
}


Результаты 0.91 у Rust, против 6.44 у Swift.

Боксинг несколько замедлил выполнение кода, но не так уж и значительно, а вот добавление inout весьма существенно повлияло на Swift. Видимо возможность поменять ссылку на canvas связывает оптимизатору руки.

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

В завершении хотелось бы сравнить, как Rust и Swift смотрятся на фоне старичка C++.
Вполне обыденный код бенчмарка
#include <iostream>
#include <vector>
#include <cassert>
#include <memory>
#include <cstdlib>

template <typename T>
struct GenericPixmap
{
    size_t w, h;
    std::vector<T> data;

    GenericPixmap(size_t w_, size_t h_, T fill_data = T()) :
        w(w_), h(h_),
        data(w_*h_, fill_data)
    {

    }

    T* operator[] (size_t i)
    {
        return &data[i*w];
    }
};

struct Vec3
{
    int x, y, z;

    int& operator[] (size_t i) 
    {
        assert(i >=0 && i < 3);
        switch (i) {
            case 0: return x;
            case 1: return y;
            case 2: return z;
            default: break;
        }
        return z;
    }
};

bool operator== (const Vec3 &a, const Vec3 &b)
{
    return a.x == b.x && a.y == b.y && a.z && b.z;
}
bool operator!= (const Vec3 &a, const Vec3 &b)
{
    return !(a == b);
}

struct RasterState
{
    Vec3 step;
    Vec3 d;
    size_t majorAxis;
};

struct LineRaster
{
    Vec3 from;
    Vec3 to;
    
    bool firstStep;
    RasterState state;

    LineRaster(const Vec3 &f, const Vec3 &t) : from(f), to(t), firstStep(true), state{}
    {}

    bool next_point()
    {
        if (firstStep) {
            size_t max = 0;
            for (int i = 0; i < 3; ++i) {
                auto d = to[i] - from[i];
                state.step[i] = d > 0 ? 1 : -1;

                d = std::abs(d);
                if (d > max) {
                    max = d;
                    state.majorAxis = i;
                }
            }
            firstStep = false;
            return true;
        } else {
            if (from == to)
                return false;
            else {
                auto calc_rs = [this](auto axis) { return std::abs(to[axis] - from[axis]); };

                from[state.majorAxis] += state.step[state.majorAxis];
                auto rs_base = calc_rs(state.majorAxis);
                for (int i = 0; i < 3; ++i) {
                    auto rs = calc_rs(i);
                    
                    if (rs > 0 && i != state.majorAxis) {
                        state.d[i] += rs;
                        if (state.d[i] >= rs_base) {
                            state.d[i] -= rs_base;
                            from[i] += state.step[i];
                        }
                    }
                }
                return true;
            }
        }
    }
};

using Pixmap = GenericPixmap<uint32_t>;

void test_code(Pixmap &pixmap)
{
    Vec3 a { 0, 0, 0 };
    Vec3 b { 50, 55, -20 };

    LineRaster raster(a, b);
    while (raster.next_point()) {
        const auto &p = raster.from;
        pixmap[p.x][p.y] = 0xFFFFFF;
    }
}

int main(int, char **) {
    Pixmap pixmap(300, 300, 0);

    Vec3 a { 0, 0, 0 };
    Vec3 b { 10, 5, -4 };

    LineRaster raster(a, b);
    while (raster.next_point()) {
        const auto &p = raster.from;

        uint32_t color = 0xffffff;
        pixmap[p.x][p.y] = color;
        std::cout << "C++: point x:" << p.x << " y:" << p.y << " z:" << p.z << " color:" << color << std::endl;
    }

    for (size_t i = 0; i < 1000000; ++i)
        test_code(pixmap);
}


В процессе написания столкнулся с несколькими ошибками сегментации и даже пришлось поотлаживать в lldb, прежде, чем получить результат.

0.79 у Rust, против 0.31 у gcc.

Результат очень интересен: с одной стороны Rust практически идентичную с clang скорость показывает, но с другой стороны gcc просто превзошел всех. То есть в целом платформе llvm есть куда стремится, но в рамках неё Rust уже в спину дышит clang'у, а это значит, что на нем уже можно вполне смело начинать писать критические по требованиям производительности участки.

Полный код бенчмарков лежит на github'е. Предложения об улучшении производительности принимаются.

К сожалению, сложно рассмотреть в одной статье всё, за бортом остались такие интересные вещи, как обработка ошибок, инфраструктура, связь с кодом на C или ObjectiveC, свойства, и многое многое другое и я надеюсь, что смогу коснуться этих вещей в будущем.

Выводы



Разработчики Swift'а заметно больше уделяют времени читабельности кода, добавляя множество синтаксического сахара в язык. Rust же более аскетичен в этом вопросе старается обойтись наименьшим количеством сущностей. Впрочем, многие вещи можно добавить при помощи макросов или плагинов к компилятору. За счет эффективных zero cost абстракций код на Rust'е получается более производительным, но зато и более сложным для восприятия. Swift же позволяет писать программы меньше обращая внимание на управление памятью, код при этом получается весьма и весьма эффективным, хотя и уступает по этому показателю Rust'у.

Что касается инфраструктуры, то для Swift'а есть прекрасная IDE XCode, но доступная лишь для OS X, а привязки с Cocoa позволяют уже сейчас писать и выпускать графические приложения для iOS или OS X. Rust же может похвастаться своим сargo и crates.io, где обитает и развивается огромное количество библиотек и приложений, но пока среди них нет ни развитой IDE ни хорошего GUI фреймворка.

К сожалению ничего нельзя сказать насчет многопоточного программирования потому, что в Swift'е пока нет родной поддержки, только через Grand Central Dispatch, но подход Rust'а позволяет писать одновременно быстрые и надежные приложения. Строгий компилятор просто убережет вас от ошибок синхронизации.

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

Послесловие



Бескомпромиссен и аскетичен Rust, Светлой стороне благоприятен он, Swift же сахаром синтаксическим своим на Темную сторону завести может.

Update
Пользователь Yarry прислал мне pull request, который сумел ускорить Swift версию в два раза путем замены классов на структуру. Структуры в Swift'е ведут себя как свои аналоги из мира C, они выделяются на стеке и для них нет необходимости в подсчете ссылок потому, что они копируются при передаче в функцию. Но при этом документация Swift утверждает, что в реальности копирование происходит лишь когда это требуется. Таким образом, при знании подобных тонкостей, можно существенно ускорить код. С другой стороны в Rust'е вы вынуждены об этом думать явно, что хоть и делает кривую обучения более крутой, зато потом придется меньше возиться с профилированием и всякими экспериментами по оптимизации кода.
Алексей Сидоров @Gorthauer87
карма
46,2
рейтинг 8,1
Программист
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Реклама

Самое читаемое Разработка

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

  • 0
    Мне кажется вы знаете rust лучше чем swift, поэтому не особо объективно
    • +6
      А можно чуть поподробнее? Особенно мне интересно, как сделать так, чтобы Брезенхем на swift'е заработал столь же быстро, как на rust'е, при этом без потери выразительности.
  • –3
    Что только не изобретают, чтобы не пользоваться Smalltalk ;-)
    • +7
      А насколько он быстрый код генерит?
      • –6
        Байткод, аналогичный Java, или любому иному, в новых виртуальных машинах по умолчанию включён режим JIT компиляции.
      • –2
        А вообще, даже лучше, чем у многих www.cincomsmalltalk.com/main/products/why-smalltalk/cincom-smalltalk-vs-java
        • +4
          По ссылке какой-то дремучий маркетинг и ни одной цифры
  • +5
    Чувствую буду заминусован, но мне человеку привыкшему к C/Delphi очень сложно воспринимать этот синтаксис, что у Rust, что у Swift. На мой взгляд переборщили с синтаксическим сахаром.
    Интересно какой синтаксис у нового поколения программистов меньше проблем вызовет?
    • +5
      В ржавчине переборщили с сахаром? О.о Обычно на обратное ругаются, что «очевидные компилятору вещи» нужно явно выписывать и «синтаксической соли» (типа `let mut`) слишком много)

      А что именно мешает воспринимать код? Мне в голову в первую очередь локальный вывод типов приходит, но он во все языки сейчас потихоньку просачивается, вроде как уже норма (`auto auto(auto) auto` :) ).
      • 0
        Лично мне мешает воспринимать код огромное количество «сахара» вроде x=>x:x, в управляющих структурах с семантикой ассоциативных структур данных, вроде match. После лиспа в котором все ассоциативные структуры любого рода и назначения выглядят просто как {ключ значение} фильтровать эту визуальную шелуху очень тяжело. В том же ML и F# с сахаром дела обстоят куда как лучше.
        • +3
          Сложно сказать, в match получаются очень аккуратные таблички, значительно упрощающие визуальный разбор.
          Лапша из if() elseif() или case: break — явно проигрывает по читабельности.
        • 0
          Но это не ассоциативные структуры а pattern matching, и по моему выглядят вполне естественно.
          «Сахара» rust очень и очень мало, то что можно назвать «сахаром» есть макросы.
          • 0
            В лиспе сопоставление с образцом реализуется как макрос над ассоциативной структурой данных которая состоит из образцов и соответствующих значений: (match val {pat val pat val ...}).
      • 0
        Ну вот по поводу очевидных вещей, дописал небольшой update. Иной раз для нас то вещи кажутся очевидными, а для компилятора нет.
    • +3
      Не знаю, как у нового поколения, а как человек, начинавший с паскаля в 90-х и десяток лет посвятивший Делфи и перепробовавший разных языков, скажу: Rust весьма нагружен низкоуровневым синтаксисом, позволяющим контролировать многое. Это делает его итак не самым лаконичным языком. Поэтому компактное описание управляющих структур, возможность выносить многое «за скобки» макросов — тут только идёт на пользу языку. А в целом — «сахара» как такового не так уж и много. Хотя, конечно, всё относительно, но сравнивать с любимым языком, не вникнув в изучаемый — не перспективно. Первое время изучая новый язык тащишь в него привычный подход и отработанные шаблоны кода, которые обычно плохо ложатся на внутреннюю логику другого языка, поэтому на предвзятый взгляд этот «другой язык» может казаться странным и уродливым, а местами очень нелогичным. Этот этап так или иначе нужно пройти, прежде чем сделать объективный вывод.
    • +1
      Я вообще, так сказать, мимопроходил (программирование для меня скорее хобби), но как по мне, Swift значительно читабельнее Rust'а со всеми этими его fn apply<F: 'static>(f: F, v1: i32) -> Box<Fn(i32) -> ()>
      • 0
        Просто, раст намного сложнее, отсюда и более сложный синтаксис.

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.