Boost.Variant Vs Virtual Interface Performance
Asked Answered
T

2

10

I'm trying to measure a performance difference between using Boost.Variant and using virtual interfaces. For example, suppose I want to increment different types of numbers uniformly, using Boost.Variant I would use a boost::variant over int and float and a static visitor which increments each one of them. Using class interfaces I would use a pure virtual class number and number_int and number_float classes which derive from it and implement an "increment" method.

From my testing, using interfaces is far faster than using Boost.Variant. I ran the code at the bottom and received these results:
Virtual: 00:00:00.001028
Variant: 00:00:00.012081

Why do you suppose this difference is? I thought Boost.Variant would be a lot faster.

** Note: Usually Boost.Variant uses heap allocations to guarantee that the variant would always be non-empty. But I read on the Boost.Variant documentation that if boost::has_nothrow_copy is true then it doesn't use heap allocations which should make things significantly faster. For int and float boost::has_nothrow_copy is true.

Here is my code for measuring the two approaches against each other.

#include <iostream>

#include <boost/variant/variant.hpp>
#include <boost/variant/static_visitor.hpp>
#include <boost/variant/apply_visitor.hpp>

#include <boost/date_time/posix_time/ptime.hpp>
#include <boost/date_time/posix_time/posix_time_types.hpp>
#include <boost/date_time/posix_time/posix_time_io.hpp>

#include <boost/format.hpp>

const int iterations_count = 100000;

// a visitor that increments a variant by N
template <int N>
struct add : boost::static_visitor<> {
    template <typename T>    
    void operator() (T& t) const {
        t += N;
    }
};

// a number interface
struct number {        
    virtual void increment() = 0;
};

// number interface implementation for all types
template <typename T>
struct number_ : number {
    number_(T t = 0) : t(t) {}
    virtual void increment() {
        t += 1;
    }
    T t;
};

void use_virtual() {
    number_<int> num_int;
    number* num = &num_int;

    for (int i = 0; i < iterations_count; i++) {
        num->increment();
    }
}

void use_variant() {
    typedef boost::variant<int, float, double> number;
    number num = 0;

    for (int i = 0; i < iterations_count; i++) {
        boost::apply_visitor(add<1>(), num);
    }
}

int main() {
    using namespace boost::posix_time;

    ptime start, end;
    time_duration d1, d2;

    // virtual
    start = microsec_clock::universal_time();
    use_virtual();
    end = microsec_clock::universal_time();

    // store result
    d1 = end - start;

    // variant
    start = microsec_clock::universal_time();
    use_variant();
    end = microsec_clock::universal_time();

    // store result
    d2 = end - start;

    // output
    std::cout << 
        boost::format(
            "Virtual: %1%\n"
            "Variant: %2%\n"
        ) % d1 % d2;
}
Treehopper answered 10/8, 2012 at 17:46 Comment(0)
T
15

For those interested, after I was a bit frustrated, I passed the option -O2 to the compiler and boost::variant was way faster than a virtual call.
Thanks

Treehopper answered 19/8, 2012 at 17:59 Comment(4)
Thanks for posting the follow-up, I was interested!Propylaeum
What were your results and what compiler? Using Boost 1.52 and Mingw 4.7 I get variant being about 8 times slower in release mode. Strangely enough -O2 is slightly faster than -O3 ;/Runoff
I'm using g++ 4.7 and I'm not sure what Boost version but it's probably 1.5x. I passed -O2 to the compiler and my results were: Virtual: 00:00:00.018806 Variant: 00:00:00.000001 Most of the times I would get 00:00:00 on the variant so I set iterations_count to 10000000. I'm running this test on a 2.8Ghz Intel Core i7 CPU.Treehopper
Also I just now noticed that if I do the use_variant test before the use_virtual test then variant slows down to about 00:00:00.000426 which is weird..Treehopper
U
4

This is obvious that -O2 reduces the variant time, because that whole loop is optimized away. Change the implementation to return the accumulated result to the caller, so that the optimizer wouldn't remove the loop, and you'll get the real difference:

Output:
Virtual: 00:00:00.000120 = 10000000
Variant: 00:00:00.013483 = 10000000

#include <iostream>

#include <boost/variant/variant.hpp>
#include <boost/variant/static_visitor.hpp>
#include <boost/variant/apply_visitor.hpp>

#include <boost/date_time/posix_time/ptime.hpp>
#include <boost/date_time/posix_time/posix_time_types.hpp>
#include <boost/date_time/posix_time/posix_time_io.hpp>

#include <boost/format.hpp>

const int iterations_count = 100000000;

// a visitor that increments a variant by N
template <int N>
struct add : boost::static_visitor<> {
    template <typename T>
    void operator() (T& t) const {
        t += N;
    }
};

// a visitor that increments a variant by N
template <typename T, typename V>
T get(const V& v) {
    struct getter : boost::static_visitor<T> {
        T operator() (T t) const { return t; }
    };
    return boost::apply_visitor(getter(), v);
}

// a number interface
struct number {
    virtual void increment() = 0;
};

// number interface implementation for all types
template <typename T>
struct number_ : number {
    number_(T t = 0) : t(t) {}
    virtual void increment() { t += 1; }
    T t;
};

int use_virtual() {
    number_<int> num_int;
    number* num = &num_int;

    for (int i = 0; i < iterations_count; i++) {
        num->increment();
    }

    return num_int.t;
}

int use_variant() {
    typedef boost::variant<int, float, double> number;
    number num = 0;

    for (int i = 0; i < iterations_count; i++) {
        boost::apply_visitor(add<1>(), num);
    }

    return get<int>(num);
}
int main() {
    using namespace boost::posix_time;

    ptime start, end;
    time_duration d1, d2;

    // virtual
    start = microsec_clock::universal_time();
    int i1 = use_virtual();
    end = microsec_clock::universal_time();

    // store result
    d1 = end - start;

    // variant
    start = microsec_clock::universal_time();
    int i2 = use_variant();
    end = microsec_clock::universal_time();

    // store result
    d2 = end - start;

    // output
    std::cout <<
        boost::format(
            "Virtual: %1% = %2%\n"
            "Variant: %3% = %4%\n"
        ) % d1 % i1 % d2 % i2;
}
Unto answered 29/3, 2015 at 15:23 Comment(2)
Doesn't that still show virtual is 2 orders of magnitude faster?Jonjona
Are you sure the compiler is not devirtualizing?Wriest

© 2022 - 2024 — McMap. All rights reserved.