Compare Swift and Rust

The reason for writing the article was the publication of the source code of the Swift language - it became interesting for me to get to know it better. The similarity of syntax with another young programming language called Rust immediately caught my eye, and besides similar syntax constructs, a similar field of application of these languages was also seen. Both languages have strong static typing with local type inference, both are compiled directly into machine code. Both languages absorbed many tricks from the world of functional programming. Both Swift and Rust have tools for running C code, which makes it easy to write wrappers over a huge number of libraries. Both languages are considered as a replacement for existing system languages such as C, C ++, ObjectiveC. So what is common in them, and what is different?
Syntax Basics
I must say right away that I’m not trying to tell readers the basics of two programming languages at once, if something is unclear, I recommend contacting Rustbook or Swiftbook for help .
First, let's try to compare the simplest programs:
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)")
You may notice that let means the same thing both there and there, and the var keyword in Swift is similar to the combination let mut in Rust. But there are differences: in Rust, in all numerical types, the size is unambiguously indicated, while Swift follows the traditions of C. In this question, Rust seems to be lower level.
String interpolation in Rust is done using the format !, macro, which is called in the bowels of println !, and in Swift, this is a feature of the language itself, but at the same time I did not find ways to set formatting options in the documentation.
The difference in the interpretation of ";" is interesting: for Rust, this is a sign of the end of the expression, which allows you to do some elegant things, for example, the last expression inside the function automatically becomes the return value. Swift simply follows the traditions of pascal, where the semicolon simply separates the operators on the same line.
Now let's try to create a function that takes a function of two arguments and converts it to a function of one:
Rust
fn apply(f: F, v1: i32) -> Box ()>
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)
The differences are already clearly noticeable here, I will leave behind purely syntactic differences such as external names for named parameters in Swift or generalizations in Rust, and move on to consider a more significant difference. The Swift approach is noticeably higher-level: the compiler decides how to store our resulting function, but for Rust it had to be packaged explicitly in box, because moved closures are dimensionless type.
Let's check whose lambda functions are faster:
fn apply(f: F, v1: i32) -> Box 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)");
Final result: 4.0 seconds for Rust versus 1.17 for Swift. It turns out that in the case of a more abstract code, the compiler has more opportunities for optimization, but on the ruRust / general channel I was prompted with a method that may not look so beautiful, but it allows the optimizer to give it all in full. Ultimately, Rust found the whole loop right at compile time, which is really cool.
In skillful hands, Rust lets you do wonders.
struct Curry<'a> {
f: &'a Fn(i32, i32) -> i32,
v1: i32
}
impl<'a> Curry<'a> {
fn new(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);
}
Enumerations and pattern matching:
Both languages have great potential for pattern matching; in both languages there are algebraic data types. Let's try to write a simple example that uses these features: try to implement mathematical operations. To do this, we will need to make our transfers recursive. As a bonus, we will try to print the names of our operations on the screen using protocols (traits).
Rust
enum MathOperation {
Value(i32),
Sum(Box, Box),
Mul(Box, Box)
}
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())");
We can notice that in Rust, to organize recursive enumerations, it was necessary to pack them into a Box container, because their size can be arbitrary and we cannot know at the compilation stage how much memory will be needed for this. Swift uses the word indirect to refer to recursive enumerations, but given its memory model, the question arises: is the compiler unable to figure out how to allocate memory? Apparently this keyword is entered more likely for the person.
We can also see that impl and extension, in principle, perform approximately the same work, and types are similar to protocols. But in Swift, the approach is more compromise: it is not necessary to add methods as extensions, they can be specified directly in the listing declaration.
Now, let's just look at a couple of pattern matching examples:
Rust (examples taken from 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 (sample code taken from 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
Here, in general, everything is very similar. There should be no dips in the comparison, you can specify ranges of values, it is possible to bind values, you can use tuples in the comparison. Although Rust also allows you to use whole structures when matching. You can specify additional conditions through if and where, respectively. But at the same time, there are additional flow control operators in Swift. Although I'm not sure that using them is a good idea.
More real example
Let's try to write the Bresenham rasterization algorithm . Only not in the usual form, but in the form for n-dimensional vectors and without the use of real numbers. This can all come in handy when studying a short course in computer graphics .
To get started, just try to create a 3-dimensional vector and determine the operations of index taking and comparison for it:
Rust
#[derive(Copy, Clone, PartialEq)]
struct Vec3 {
x: i32,
y: i32,
z: i32
}
impl Index 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 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)
}
In Rust, operators are actually implemented through traits; if a structure implements the Index trait, the [] operator can be used for it; in Swift, operators for taking index are written using the keyword subscript and allow you to add special preconditions. Rust relies on regular assertions. In the case of other operators, Swift allows both redefining existing ones and defining their own, moreover, indicating priorities, associativity. All this can help a lot when writing math libraries, making the code more similar to the original math expressions. Rust, however, can automatically create implementations of some types through the attribute derive, which allows you to avoid writing code yourself in many trivial cases.
Let's now try to create a convenient buffer for storing the image so that the color of any pixel can be accessed through index operators. At the same time, we will put forward the requirement that the pixel map is capable of different color depths, so we will make it generalized:
rust
struct GenericPixmap {
w: usize,
h: usize,
data: Vec
}
impl GenericPixmap
where T: Copy + Clone
{
fn new(w: usize, h: usize, fill_value: T) -> GenericPixmap {
GenericPixmap {
w: w,
h: h,
data: vec![fill_value; w*h]
}
}
}
impl Index for GenericPixmap
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 IndexMut for GenericPixmap
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;
swift
struct GenericPixmap {
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 The rules for generalizations in Rust are more stringent and we need to explicitly indicate that the template type must be able to copy and create its own copy. In Swift, for structure fields, you can explicitly set their mutability. You can also notice the init keyword. This is a constructor of a class or structure; there can be several of them; they can delegate their authority to each other. As a result, this translates into a rather complex and multi-stage process, which, nevertheless, accurately guarantees that each member will be initialized. In Rust, there is a term-wise initialization and agreement that the object should be created by the static function new. If the process promises to be complicated, it is recommended to use factories. As for static functions, Rust's syntax in this sense follows the traditions of python, while Swift is C ++.
I want to note that the index operator in Swift can take any number of arguments of any type, so there you can write an operator that immediately receives a specific element of the array, but in Rust you need to create a slice.
Now let's create the Canvas trait, which allows us to draw without thinking about the implementation of the process itself.
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
}
}
Unfortunately, I could not quickly figure out how to implement the extension for the GenericPixmap type, so I decided to create a new MyCanvas class that implements the Canvas protocol, unlike Rust in Swift, you can inherit from protocols and more.
Now we come to the most interesting - the implementation of the Bresenham algorithm. We want to draw a line from the point (x1, y1, z1) to the point (x2, y2, z2), for this we need to take (| x2-x1 |, | y2-y1 |, | z2-z1 |) steps in the direction , which depends on the signs of the expression (x2-x1, y2-y1, z2-z1).
So, we need to go through (rx, ry, rz) steps in the directions (sx, sy, sz), for this we find the axis along which we need to make the greatest number of steps. The displacement at each step will be equal to (rx / r [max, ry / r [max], rz / r [max]), and the step will only occur if the total displacement d has become greater than one, then a step is made along the axis, and from total displacement unit is deducted. I.e:
d[i] += r[i] / r[max]
if d[i] >= 1 { r[i] -= s[i]; d[i] -= 1; }
It is easy to see that if we multiply the condition by rmax, then we can do without the division operation at all.
d[i] += r[i]
if d[i] >= r[max] { r[i] -= s[i]; d[i] -= r[max]; }
As a result, this variant of the algorithm works for any number of coordinates and avoids floating point operations, which are almost always more expensive than operations with integers.
Let's try to write a generator that creates a sequence of points between the end nodes a and b. In this case, point a will be included in the sequence.
rust
struct RasterState {
step: Vec3,
d: Vec3,
major_axis: usize,
}
struct LineRasterizer {
from: Vec3,
to: Vec3,
state: Option
}
impl LineRasterizer {
fn new(from: Vec3, to: Vec3) -> LineRasterizer {
LineRasterizer {
from: from,
to: to,
state: None
}
}
fn next_point(&mut self) -> Option {
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
}
}
}
I decided to make the state of the generator in the form of an optional value, this allows us to easily and immediately return the starting point from from the generator without the need for additional flags. In Rust, optional values are made simply through the enum Option, while in Swift they are part of the language, which makes it easy to describe optional call chains without too much syntax noise.
Rust uses an advanced tenure system to tell it that we borrow State from an enumeration by reference, you need to write the keyword ref. In Swift, State is by default a reference data type, and move semantics are not yet observed in the language, so we can just take and unpack state without worrying about anything.
Write code like:
while let Some(point) = rasterizer.next_point() { ... }
It seems to me not too elegant, it looks much more logical for this.
for point in generator { ... }
Fortunately, in order to use the for loop, it’s quite simple to implement several traits for our generator.
rust
impl Iterator for LineRasterizer
{
type Item = Vec3;
fn next(&mut self) -> Option {
self.next_point()
}
}
swift
extension LineRaster : GeneratorType {
func next() -> Vector3? {
return self.next_point()
}
}
extension LineRaster : SequenceType {
func generate() -> LineRaster {
return self
}
}
Moreover, for Swift, you need to implement as many as two protocols, while for Rust it is enough one, but it does not make any fundamental difference.
Let's measure performance a little
The speed of code execution is one of the important factors when comparing different programming languages, and it would be foolish to write an article without taking performance measurements.
Naive comparison:
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)
}
We simply pass the link to the Canvas into the function we are testing and measure the time.
It turned out 0.86 for Rust against 5.3 for Swift. It is likely that Rust somehow inline calls, while Swift remained at the level of dynamic dispatching. To verify this, we try to write a generalized function.
rust
fn test_code_generic(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(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)
}
}
Rust's results are 0.83 versus Swift's 4.94, which tells us that after all, Swift was able to better optimize the code, but somewhere else there were bottlenecks that he could not figure out.
Now let's try packing the Canvas in a box, and for Swift, use the inout modifier, which in its action is similar to & mut.
rust
fn test_code_boxed(canvas: &mut Boxswift
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)
}
}
Results 0.91 for Rust, versus 6.44 for Swift.
Boxing slowed down the execution of the code a little, but not so much, but the addition of inout very significantly affected Swift. Apparently the ability to change the link to canvas binds the hand to the optimizer.
In general, Rust is more productive with a similar coding style. The ownership concept allows the compiler not only to check the correctness of the program at the compilation stage, but also to avoid using the reference counter in those cases when it is not needed, on the contrary you will obviously have to use it if you need it. Swift, by default, everywhere uses a reference counter for classes, which in many cases leads to performance degradation, and even if the compiler manages to remove reference counting in some trivial cases, this will not guarantee that it can do it in slightly more complex ones. Rust just won't let the code compile. This indicates both a greater maturity of the compiler and the effectiveness of the concepts contained in it.
In conclusion, I would like to compare how Rust and Swift look against the background of the old C ++.
#include
#include
#include
#include
#include
template
struct GenericPixmap
{
size_t w, h;
std::vector 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;
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);
}
In the process of writing, I encountered several segmentation errors and even had to debug in lldb before getting the result.
0.79 for Rust, versus 0.31 for gcc.
The result is very interesting: on the one hand, Rust shows almost identical speed with clang, but on the other hand, gcc simply surpassed all. That is, in general, the llvm platform has a lot to strive for, but within the framework of it, Rust is already breathing clang in the back, which means that you can already safely start writing critical sections of it according to performance requirements.
The full benchmark code is on github . Suggestions for performance improvements are accepted.
Unfortunately, it’s difficult to consider everything in one article, such interesting things as error handling, infrastructure, communication with C or ObjectiveC code, properties, and much much more were left behind and I hope that I can touch on these things in the future.
conclusions
Swift developers noticeably spend more time reading code by adding lots of syntactic sugar to the language. Rust is more ascetic in this matter, trying to get by with the least number of entities. However, many things can be added using macros or plugins to the compiler. Due to the effective zero cost abstractions, Rust code is more productive, but more difficult to understand. Swift allows you to write programs less paying attention to memory management, while the code is very, very effective, although it is inferior to Rust in this respect.
As for the infrastructure, Swift has an excellent Xcode IDE, but is available only for OS X, and Cocoa bindings now allow you to write and release graphical applications for iOS or OS X. Rust can also boast its cargo and crates.io, where a huge number of libraries and applications live and develop, but so far there is neither a developed IDE nor a good GUI framework among them.
Unfortunately, nothing can be said about multi-threaded programming because Swift has no native support yet, only through Grand Central Dispatch, but Rust's approach allows you to write both fast and reliable applications. A strict compiler will simply save you from synchronization errors.
Jobs at Swift are now much larger, and it’s easier to learn, but this does not mean that Rust specialists will not be in demand in the near future. I think in the world there will be many problems in which both of these languages will mercilessly compete with each other and, I hope, it will benefit them.
Afterword
The Rust is uncompromising and ascetic, It is favorable to the Light side, Swift can bring its syntactic sugar to the Dark side.
Update
User Yarrysent me a pull request, which managed to speed up the Swift version twice by replacing the classes with a structure. Structures in Swift behave like their counterparts from the C world, they are allocated on the stack and there is no need for them to count the links because they are copied when passed to the function. But at the same time, the Swift documentation claims that in reality, copying only happens when it is needed. Thus, with knowledge of such subtleties, you can significantly speed up the code. On the other hand, in Rust you are forced to think about it explicitly, which, although it makes the learning curve steeper, but then you will have to bother less with profiling and all sorts of experiments to optimize the code.