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.