methcall

Full source code

C++

// -*- mode: c++ -*-
// $Id
// http://www.bagley.org/~doug/shootout/

// with some help from Bill Lear

#include <stdlib.h>
#include <iostream>

using namespace std;

class Toggle {
public:
    Toggle(bool start_state) : state(start_state) { }
    virtual ~Toggle() {  }
    bool value() {
        return(state);
    }
    virtual Toggle& activate() {
        state = !state;
        return(*this);
    }
    bool state;
};

class NthToggle : public Toggle {
public:
    NthToggle(bool start_state, int max_counter) :
        Toggle(start_state), count_max(max_counter), counter(0) {
    }
    Toggle& activate() {
        if (++this->counter >= this->count_max) {
            state = !state;
            counter = 0;
        }
        return(*this);
    }
private:
    int count_max;
    int counter;
};


int
main(int argc, char *argv[]) {
#ifdef SMALL_PROBLEM_SIZE
#define LENGTH 100000000
#else
#define LENGTH 1000000000
#endif
    int n = ((argc == 2) ? atoi(argv[1]) : LENGTH);

    bool val = true;
    Toggle *toggle = new Toggle(val);
    for (int i=0; i<n; i++) {
        val = toggle->activate().value();
    }
    cout << ((val) ? "true" : "false") << endl;
    delete toggle;

    val = true;
    NthToggle *ntoggle = new NthToggle(val, 3);
    for (int i=0; i<n; i++) {
        val = ntoggle->activate().value();
    }
    cout << ((val) ? "true" : "false") << endl;
    delete ntoggle;

    return 0;
}

Rust

// Adapted from https://github.com/llvm/llvm-test-suite and
// http://www.bagley.org/~doug/shootout/
// with some help from Bill Lear
use std::env;

#[cfg(feature = "small_problem_size")]
const LENGTH: i32 = 100000000;

#[cfg(not(feature = "small_problem_size"))]
const LENGTH: i32 = 1000000000;

struct Toggle {
    state: bool
}

impl Toggle {
    fn new(start_state: bool) -> Toggle {
        Toggle { state: start_state }
    }
}

trait Togglable {
    fn activate(&mut self);
    fn value(&self) -> bool;
}

impl Togglable for Toggle {
    fn activate(&mut self) {
        self.state = !self.state;
    }
    fn value(&self) -> bool {
        return self.state;
    }
}

struct NthToggle {
    state: bool,
    count_max: i32,
    counter: i32,
}

impl NthToggle {
    fn new(start_state: bool, max_counter: i32) -> NthToggle {
        NthToggle {
            state: start_state,
            count_max: max_counter,
            counter: 0,
        }
    }
}

impl Togglable for NthToggle {
    fn activate(&mut self) {
        self.counter += 1;
        if self.counter >= self.count_max {
            self.state = !self.state;
            self.counter = 0;
        }
    }
    fn value(&self) -> bool {
        return self.state;
    }
}

fn main() {
    let mut args = env::args();
    let n = if args.len() == 2 {
        args.nth(1).unwrap().parse::<i32>().unwrap()
    } else {
        LENGTH
    };

    let mut val = true;
    let mut toggle = Toggle::new(val);
    for _i in 0..n {
        toggle.activate();
        val = toggle.value();
    }
    println!("{}", if val { "true" } else { "false" });

    val = true;
    let mut ntoggle = NthToggle::new(val, 3);
    for _i in 0..n {
        ntoggle.activate();
        val = ntoggle.value();
    }
    println!("{}", if val { "true" } else { "false" });
}

Porting notes

Declaring a struct in place for a class

C++

class Toggle {
public:
    Toggle(bool start_state) : state(start_state) { }
    virtual ~Toggle() {  }
    bool value() {
        return(state);
    }
    virtual Toggle& activate() {
        state = !state;
        return(*this);
    }
    bool state;
};

Rust

#![allow(unused)]
fn main() {
struct Toggle {
    state: bool
}

impl Toggle {
    fn new(start_state: bool) -> Toggle {
        Toggle { state: start_state }
    }
}

trait Togglable {
    fn activate(&mut self);
    fn value(&self) -> bool;
}

impl Togglable for Toggle {
    fn activate(&mut self) {
        self.state = !self.state;
    }
    fn value(&self) -> bool {
        return self.state;
    }
}
}

There are no classes and structs are used in Rust.

Member variables and methods aren't placed together in a Rust struct. Member variables are declared in the struct and member methods are declared in the impl block for the struct.

The new method, which is like a C++ constructor, is usually used to create an instance of the struct. The struct values are often initialized with struct literals, as in Toggle { state: start_state }, as opposed to the C++ member-by-member style.

The drop method, which is analogous to a C++ destructor but is often omitted because the owership takes care of destruction unless the struct requires a custom logic for deinitization like resource reclamation.

The methods activate and value would be put in the impl Toggle { ... } otherwise but they are put into a trait Togglable here to handle the use of inheritance, which will be discussed next. A trait is like an interface in Go or Java. For now, note that the struct Toggle implements the trait Togglable that has the two methods activate and value and they are defined in the impl Tooglable for Toggle { ... } block.

Using a trait in place for inheritance.

C++

class NthToggle : public Toggle {
public:
    NthToggle(bool start_state, int max_counter) :
        Toggle(start_state), count_max(max_counter), counter(0) {
    }
    Toggle& activate() {
        if (++this->counter >= this->count_max) {
            state = !state;
            counter = 0;
        }
        return(*this);
    }
private:
    int count_max;
    int counter;
};

Rust

#![allow(unused)]
fn main() {
struct NthToggle {
    state: bool,
    count_max: i32,
    counter: i32,
}

impl NthToggle {
    fn new(start_state: bool, max_counter: i32) -> NthToggle {
        NthToggle {
            state: start_state,
            count_max: max_counter,
            counter: 0,
        }
    }
}

impl Togglable for NthToggle {
    fn activate(&mut self) {
        self.counter += 1;
        if self.counter >= self.count_max {
            self.state = !self.state;
            self.counter = 0;
        }
    }
    fn value(&self) -> bool {
        return self.state;
    }
}
}

Rust is not an object oriented programming lanuage. In the C++ version, the class NthToggle inherits from the class Toggle. Because Rust doesn't support inheritance, we instead use a trait, which is like interfaces in Go or Java. The activate and value methods, which are inherited or overridden, are put into a trait Togglable, as opposed to directly in the impl Toggle { ... } block. The struct NthToggle has three variables, instead of one for Toogle and it also implements the trait Togglable. The impl Togglable for NthToggle { ... } block implements the two methods for NthToggle. That way, the two methods can be separately implemented for Toggle and NthToggle and can be polymorphically called like C++ virtual calls. A caveat is that the state variable and the value method need to be duplicated.

Avoding unnecessarily returning a reference

C++

        val = toggle->activate().value();

Rust

#![allow(unused)]
fn main() {
        toggle.activate();
        val = toggle.value();
}

I chose to change the activate method not to return a reference to the instance. I believe that we would want to avoid returning a reference in Rust as it might require lifetime annotations to be used and lead to unnecessary complexity. In this example, we could simply call the value method to get the state value in the second line.

Performance analysis

The benchmark results show that the Rust version is about 8 times faster than the C++ version.

We will compare the generated code between C++ and Rust.

C++

C++ perfdata (the hot function list)

Overhead  Command   Shared Object        Symbol
  78.05%  methcall  methcall             [.] main
  12.20%  methcall  methcall             [.] NthToggle::activate()
   9.65%  methcall  methcall             [.] Toggle::activate()

C++ perfdata (the main function)

  4.86 │ 50:   mov    (%r14),%rax
  4.73 │       mov    %r14,%rdi
 40.76 │     → call   *0x10(%rax)
  2.60 │       dec    %ebp
  4.86 │     ↑ jne    50

C++ perfdata (Toggle::activate())

 21.28 │     mov  %rdi,%rax
 39.85 │     xorb $0x1,0x8(%rdi)
 38.87 │   ← ret

C++ perfdata (NthToggle::activate())

 10.18 │      mov  %rdi,%rax
  9.78 │      mov  0x10(%rdi),%ecx
 10.25 │      inc  %ecx
 21.22 │      mov  %ecx,0x10(%rdi)
  9.13 │      cmp  0xc(%rdi),%ecx
       │    ↓ jl   1b
  3.94 │      xorb $0x1,0x8(%rax)
  7.03 │      movl $0x0,0x10(%rax)
 28.45 │1b: ← ret

It looks like the activate call isn't devirtualized or inlined.

Rust

Rust perfdata (the host function list)

Overhead  Command   Shared Object     Symbol
  99.92%  methcall  methcall          [.] methcall::main

Rust perfdata (the host loop in the main function)

  2.52 │280:   xor    %dil,%cl
  4.92 │       xor    %r8b,%cl
  4.50 │       xor    %r9b,%cl
  2.40 │       xor    %r11b,%cl
  3.75 │       add    $0xfffffffc,%ebx
  3.51 │     ↑ je     23a
  4.33 │291:   lea    0x1(%rdx),%r8d
  6.20 │       cmp    $0x2,%edx
  3.23 │       setge  %dil
  7.95 │       cmovge %esi,%r8d
  3.92 │       lea    0x1(%r8),%edx
  2.63 │       cmp    $0x2,%r8d
  9.47 │       cmovge %esi,%edx
  4.91 │       setge  %r8b
  2.28 │       mov    $0x0,%r10d
  3.27 │       cmp    $0x2,%edx
  2.52 │       setge  %r9b
  3.39 │     ↓ jge    2c3
  2.75 │       inc    %edx
  1.99 │       mov    %edx,%r10d
  3.63 │2c3:   mov    $0x0,%edx
  2.92 │       cmp    $0x2,%r10d
  2.92 │       setge  %r11b
  3.80 │     ↑ jge    280
  1.93 │       inc    %r10d
  2.83 │       mov    %r10d,%edx
  1.52 │     ↑ jmp    280

We can see that the activate call is devirtualized, inlined and the loop is unrolled by a factor of 4. This explains the performance difference.