I have some code to iterate over a (multivariate) numeric range:
#include <array>
#include <limits>
#include <iostream>
#include <iterator>
template <int N>
class NumericRange : public std::iterator<double, std::input_iterator_tag>
{
public:
NumericRange() {
_lower.fill(std::numeric_limits<double>::quiet_NaN());
_upper.fill(std::numeric_limits<double>::quiet_NaN());
_delta.fill(std::numeric_limits<double>::quiet_NaN());
}
NumericRange(const std::array<double, N> & lower, const std::array<double, N> & upper, const std::array<double, N> & delta):
_lower(lower), _upper(upper), _delta(delta) {
_state.fill(std::numeric_limits<double>::quiet_NaN());
}
const std::array<double, N> & get_state() const {
return _state;
}
NumericRange<N> begin() const {
NumericRange<N> result = *this;
result.start();
return result;
}
NumericRange<N> end() const {
NumericRange<N> result = *this;
result._state = _upper;
return result;
}
bool operator !=(const NumericRange<N> & rhs) const {
return in_range();
// return ! (*this == rhs);
}
bool operator ==(const NumericRange<N> & rhs) const {
return _state == rhs._state && _lower == rhs._lower && _upper == rhs._upper && _delta == rhs._delta;
}
const NumericRange<N> & operator ++() {
advance();
if ( ! in_range() )
_state = _upper;
return *this;
}
const std::array<double, N> & operator *() const {
return _state;
}
void start() {
_state = _lower;
}
bool in_range(int index_to_advance = N-1) const {
return ( _state[ index_to_advance ] - _upper[ index_to_advance ] ) < _delta[ index_to_advance ];
}
void advance(int index_to_advance = 0) {
_state[ index_to_advance ] += _delta[ index_to_advance ];
if ( ! in_range(index_to_advance) ) {
if (index_to_advance < N-1) {
// restart index_to_advance
_state[index_to_advance] = _lower[index_to_advance];
// carry
++index_to_advance;
advance(index_to_advance);
}
}
}
private:
std::array<double, N> _lower, _upper, _delta, _state;
};
int main() {
std::array<double, 7> lower{{0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0}};
std::array<double, 7> upper{{1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0}};
std::array<double, 7> delta{{0.03, 0.06, 0.03, 0.06, 0.03, 0.06, 0.03}};
NumericRange<7> nr(lower, upper, delta);
int c = 0;
for (nr.start(); nr.in_range(); nr.advance()) {
++c;
}
std::cout << "took " << c << " steps" << std::endl;
return 0;
}
Compiling with g++ -std=c++11 -O3
(or -std=c++0x
with gcc < 4.7) runs in around 13.8 seconds on my computer.
If I change the main
function to use a range-based for loop:
for (const std::array<double, 7> & arr : nr) {
++c;
}
the runtime increases to 29.8 seconds. Coincidentally, this ~30 seconds runtime is almost the same as the runtime of the original when using std::vector<double>
instead of std::array<double, N>
, leading me to believe the compiler cannot unroll the code produced by the range-based for loop.
Is there a way to have the speed of the original and still use range-based for loops?
What I've tried:
I can get the desired speed with a range-based for loop by changing two member functions in NumericRange
:
bool operator !=(const NumericRange<N> & rhs) const {
return in_range();
// return ! (*this == rhs);
}
const NumericRange<N> & operator ++() {
advance();
// if ( ! in_range() )
// _state = _upper;
return *this;
}
However, this code feels poorly designed because the != operator
doesn't work as expected Normally for numeric operations, I use <
to terminate a loop rather than ==
. I thought of finding the first out-of-range value, but to do so analytically may not give an exact answer due to numerical error.
How can you force the != operator
to behave similarly to a <
without misleading others who will see my code? I'd simply make the begin()
and end()
functions private, but they need to be public for the range-based for loop.
Thanks a lot for your help.
Range-for is as fast as possible since it caches the end iterator[citationprovided], uses pre-increment and only dereferences the iterator once. Then, yes, range-for may be slightly faster, since it's also easier to write there's no reason not to use it (when appropriate).
Range-based for loop in C++ It executes a for loop over a range. Used as a more readable equivalent to the traditional for loop operating over a range of values, such as all elements in a container.
For the access to a an element you will have to do the index calculation yourself. But as one of the indices (j) is constant over the loop, you can compute the start element's index before the loop and inside just increment the index 1. That might get you some significant improvement.
The traditional for loop is the fastest, so you should always use that right? Not so fast - performance is not the only thing that matters. Code Readability is usually more important, so default to the style that fits your application.
The problem, as far as I am concerned, is that you are not using the range-for construct appropriately.
Let us take a step back:
void foo(std::vector<int> const& v) {
for (int i: v) {
}
}
Note how the range-for iterates over the vector to extract integers.
For some reasons you chose not to implement iterators to bridge from begin
to end
, but instead re-use a copy of what you are iterating over, even though it varies only very slightly, and you are doing a ton of extra-work (in the copy and checks)...
Note: std::iterator<double, ...>
means that operator*
should return a double&
.
If you want to use the new idiom, you will have to conform to its expectations.
The expectation is that you iterate with iterators and not by copying the original object (slightly modified) over and over. This is the C++ idiom.
It means that you will need to cut your object in half: pull off everything that is immutable during the iteration in the object to be iterated over and what is modified in the iterator.
From what I can see:
_lower
, _upper
and _delta
are fixed_state
is the iteration variableTherefore, you would have:
template <typename> class NumericRangeIterator
template <unsigned N> // makes no sense having a negative here
class NumericRange {
public:
template <typename> friend class NumericRangeIterator;
typedef NumericRangeIterator<NumericRange> iterator;
typedef NumericRangeIterator<NumericRange const> const_iterator;
static unsigned const Size = N;
// ... constructors
iterator begin(); // to be defined after NumericRangeIterator
iterator end();
const_iterator begin() const;
const_iterator end() const;
private:
std::array<double, N> _lower, _upper, _delta;
}; // class NumericRange
template <typename T>
class NumericRangeIterator: public
std::iterator< std::array<double, T::Size>,
std::forward_iterator_tag >
{
public:
template <unsigned> friend class NumericRange;
NumericRangeIterator(): _range(0), _state() {}
NumericRangeIterator& operator++() {
this->advance();
return *this;
}
NumericRangeIterator operator++(int) {
NumericRangeIterator tmp(*this);
++*this;
return tmp;
}
std::array<double, T::Size> const& operator*() const {
return _state;
}
std::array<double, T::Size> const* operator->() const {
return _state;
}
bool operator==(NumericRangeIterator const& other) const {
return _state != other._state;
}
bool operator!=(NumericRangeIterator const& other) const {
return !(*this == other);
}
private:
NumericRangeIterator(T& t, std::array<double, T::Size> s):
_range(&t), _state(s) {}
void advance(unsigned index = T::Size - 1); // as you did
void in_range(unsigned index = T::Size - 1); // as you did
T* _range;
std::array<double, T::Size> _state;
}; // class NumericRangeIterator
template <unsigned N>
auto NumericRange<N>::begin() -> typename NumericRange<N>::iterator {
return iterator(*this, _lower);
}
template <unsigned N>
auto NumericRange<N>::end() -> typename NumericRange<N>::iterator {
return iterator(*this, _upper);
}
And with all this setup, you can write:
for (auto const& state: nr) {
}
Where auto
will be deduced to be std::array<double, nr::Size>
.
Note: not sure the iterator
are useful, maybe only the const_iterator
since it's a false iteration in a way; you cannot reach into the range object to modify it through the iterator.
EDIT: operator==
is too slow, how to get it better ?
I propose to cheat.
1/ Modify the constructors of the iterator
NumericRangeIterator(): _range(0), _state() {} // sentinel value
NumericRangeIterator(T& t): _range(&t), _state(t._lower) {}
2/ Tweak the iteration to create the new "sentinel" value at the end
void advance() {
// ...
if (not this->in_range()) { // getting out of the iteration ?
*this = NumericRangeIterator(); // then use the sentinel value
}
}
3/ Change the begin
and end
definition accordingly
template <unsigned N>
auto NumericRange<N>::begin() -> typename NumericRange<N>::iterator {
return iterator(*this);
}
template <unsigned N>
auto NumericRange<N>::end() -> typename NumericRange<N>::iterator {
return iterator();
}
4/ Make ==
more equal by using the sentinel
bool operator==(NumericRangeIterator const& other) const {
return _range == other._range and _state == other._state;
}
Now, all along iteration the ==
is short-circuited because one of the _range
is null and the other is not. Only at the last call will the comparison of the two _state
attributes actually occur.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With