Skip to content

Commit 6cc4601

Browse files
authored
Fix bug in 1D histogram::fill when axis is growing and argument is single value (#334)
1 parent b11de06 commit 6cc4601

File tree

9 files changed

+127
-10
lines changed

9 files changed

+127
-10
lines changed

.github/workflows/slow.yml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,9 +64,12 @@ jobs:
6464
../../b2 $B2_OPTS cxxstd=17 test//all
6565
6666
gcc5:
67-
runs-on: ubuntu-16.04
67+
runs-on: ubuntu-18.04
6868
steps:
6969
- uses: actions/checkout@v2
70+
- uses: egor-tensin/setup-gcc@v1
71+
with:
72+
version: 5
7073
- name: Fetch Boost superproject
7174
run: |
7275
cd ..

include/boost/histogram/detail/axes.hpp

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -349,7 +349,21 @@ std::size_t bincount(const T& axes) {
349349
return n;
350350
}
351351

352-
// initial offset for the linear index
352+
/** Initial offset for the linear index.
353+
354+
This precomputes an offset for the global index so that axis index = -1 addresses the
355+
first entry in the storage. Example: one-dim. histogram. The offset is 1, stride is 1,
356+
and global_index = offset + axis_index * stride == 0 addresses the first element of
357+
the storage.
358+
359+
Using the offset makes the hot inner loop that computes the global_index simpler and
360+
thus faster, because we do not have to branch for each axis to check whether it has
361+
an underflow bin.
362+
363+
The offset is set to an invalid value when the histogram contains at least one growing
364+
axis, because this optimization then cannot be used. See detail/linearize.hpp, in this
365+
case linearize_growth is called.
366+
*/
353367
template <class T>
354368
std::size_t offset(const T& axes) {
355369
std::size_t n = 0;

include/boost/histogram/detail/fill.hpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -250,7 +250,7 @@ template <class ArgTraits, class Storage, class Axes, class Args>
250250
auto fill_2(ArgTraits, mp11::mp_true, const std::size_t, Storage& st, Axes& axes,
251251
const Args& args) {
252252
std::array<axis::index_type, ArgTraits::nargs::value> shifts;
253-
// offset must be zero for linearize_growth
253+
// offset must be zero for linearize_growth (value of offset argument is ignored)
254254
mp11::mp_if<has_non_inclusive_axis<Axes>, optional_index, std::size_t> idx{0};
255255
std::size_t stride = 1;
256256
bool update_needed = false;

include/boost/histogram/detail/fill_n.hpp

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -101,12 +101,18 @@ struct index_visitor {
101101
template <class T>
102102
void call_1(std::true_type, const T& value) const {
103103
// T is compatible value; fill single value N times
104-
index_type idx{*begin_};
105-
call_2(IsGrowing{}, &idx, value);
106-
if (is_valid(idx)) {
104+
105+
// Optimization: We call call_2 only once and then add the index shift onto the
106+
// whole array of indices, because it is always the same. This also works if the
107+
// axis grows during this operation. There are no shifts to apply if the zero-point
108+
// changes.
109+
const auto before = *begin_;
110+
call_2(IsGrowing{}, begin_, value);
111+
if (is_valid(*begin_)) {
112+
// since index can be std::size_t or optional_index, must do conversion here
107113
const auto delta =
108-
static_cast<std::intptr_t>(idx) - static_cast<std::intptr_t>(*begin_);
109-
for (auto&& i : make_span(begin_, size_)) i += delta;
114+
static_cast<std::intptr_t>(*begin_) - static_cast<std::intptr_t>(before);
115+
for (auto it = begin_ + 1; it != begin_ + size_; ++it) *it += delta;
110116
} else
111117
std::fill(begin_, begin_ + size_, invalid_index);
112118
}
@@ -129,7 +135,9 @@ void fill_n_indices(Index* indices, const std::size_t start, const std::size_t s
129135
*eit++ = axis::traits::extent(a);
130136
}); // LCOV_EXCL_LINE: gcc-8 is missing this line for no reason
131137

132-
// offset must be zero for growing axes
138+
// TODO this seems to always take the path for growing axes, even if Axes is vector
139+
// of variant and types actually held are not growing axes?
140+
// index offset must be zero for growing axes
133141
using IsGrowing = has_growing_axis<Axes>;
134142
std::fill(indices, indices + size, IsGrowing::value ? 0 : offset);
135143
for_each_axis(axes, [&, stride = static_cast<std::size_t>(1),

include/boost/histogram/detail/linearize.hpp

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,12 @@ std::size_t linearize(Index& out, const std::size_t stride, const Axis& ax,
5959
return linearize(opts, out, stride, ax.size(), axis::traits::index(ax, v));
6060
}
6161

62-
// initial offset of out must be zero
62+
/**
63+
Must be used when axis is potentially growing. Also works for non-growing axis.
64+
65+
Initial offset of `out` must be zero. We cannot assert on this, because we do not
66+
know if this is the first call of `linearize_growth`.
67+
*/
6368
template <class Index, class Axis, class Value>
6469
std::size_t linearize_growth(Index& out, axis::index_type& shift,
6570
const std::size_t stride, Axis& a, const Value& v) {

test/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ boost_test(TYPE run SOURCES indexed_test.cpp)
8989
boost_test(TYPE run SOURCES storage_adaptor_test.cpp)
9090
boost_test(TYPE run SOURCES unlimited_storage_test.cpp)
9191
boost_test(TYPE run SOURCES utility_test.cpp)
92+
boost_test(TYPE run SOURCES issue_327_test.cpp)
9293

9394
find_package(Threads)
9495
if (Threads_FOUND)

test/Jamfile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ alias cxx14 :
8787
[ run storage_adaptor_test.cpp ]
8888
[ run unlimited_storage_test.cpp ]
8989
[ run utility_test.cpp ]
90+
[ run issue_327_test.cpp ]
9091
;
9192

9293
alias cxx17 :

test/histogram_fill_test.cpp

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414
#include <boost/histogram/axis/category.hpp>
1515
#include <boost/histogram/axis/integer.hpp>
1616
#include <boost/histogram/axis/ostream.hpp>
17+
#include <boost/histogram/axis/regular.hpp>
18+
#include <boost/histogram/axis/variant.hpp>
1719
#include <boost/histogram/histogram.hpp>
1820
#include <boost/histogram/literals.hpp>
1921
#include <boost/histogram/make_histogram.hpp>
@@ -343,6 +345,61 @@ void run_tests(const std::vector<int>& x, const std::vector<int>& y,
343345
}
344346
}
345347

348+
void special_tests() {
349+
// 1D growing (bug?)
350+
{
351+
using axis_type_1 =
352+
axis::regular<double, use_default, use_default, axis::option::bitset<11u>>;
353+
using axis_type_2 = axis::regular<double>;
354+
using axis_type_3 = axis::integer<int>;
355+
using axis_type_4 = axis::category<int>;
356+
using axis_type_5 = axis::category<std::string>;
357+
358+
using axis_variant_type =
359+
axis::variant<axis_type_1, axis_type_2, axis_type_3, axis_type_4, axis_type_5>;
360+
361+
auto axes = std::vector<axis_variant_type>({axis_type_1(10, 0, 1)});
362+
auto h = histogram<decltype(axes), dense_storage<double>>(axes);
363+
auto h2 = h;
364+
365+
std::vector<int> f1({2});
366+
std::vector<int> f2({-1});
367+
368+
h(2);
369+
h(-1);
370+
371+
h2.fill(f1);
372+
h2.fill(f2);
373+
374+
BOOST_TEST_EQ(h, h2);
375+
BOOST_TEST_EQ(sum(h2), 2);
376+
}
377+
378+
// 1D growing (bug?)
379+
{
380+
using axis_type_1 =
381+
axis::regular<double, use_default, use_default, axis::option::bitset<11u>>;
382+
383+
using axis_variant_type = axis::variant<axis_type_1>;
384+
385+
auto axes = std::vector<axis_variant_type>({axis_type_1(10, 0, 1)});
386+
auto h = histogram<decltype(axes), dense_storage<double>>(axes);
387+
auto h2 = h;
388+
389+
std::vector<int> f1({2});
390+
std::vector<int> f2({-1});
391+
392+
h(2);
393+
h(-1);
394+
395+
h2.fill(f1);
396+
h2.fill(f2);
397+
398+
BOOST_TEST_EQ(h, h2);
399+
BOOST_TEST_EQ(sum(h2), 2);
400+
}
401+
}
402+
346403
int main() {
347404
std::mt19937 gen(1);
348405
std::normal_distribution<> id(0, 2);
@@ -357,5 +414,7 @@ int main() {
357414
run_tests<static_tag>(x, y, w);
358415
run_tests<dynamic_tag>(x, y, w);
359416

417+
special_tests();
418+
360419
return boost::report_errors();
361420
}

test/issue_327_test.cpp

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
#include <boost/core/lightweight_test.hpp>
2+
#include <boost/histogram.hpp>
3+
#include <boost/histogram/ostream.hpp>
4+
#include <vector>
5+
#include "throw_exception.hpp"
6+
7+
namespace bh = boost::histogram;
8+
using uogrowth_t = decltype(bh::axis::option::growth | bh::axis::option::underflow |
9+
bh::axis::option::overflow);
10+
11+
using arg_t = boost::variant2::variant<std::vector<int>, int>;
12+
13+
int main() {
14+
using axis_type =
15+
bh::axis::regular<double, bh::use_default, bh::use_default, uogrowth_t>;
16+
using axis_variant = bh::axis::variant<axis_type>;
17+
18+
auto axes = std::vector<axis_variant>({axis_type(10, 0, 1)});
19+
auto h = bh::make_histogram_with(std::vector<int>(), axes);
20+
BOOST_TEST_EQ(h.rank(), 1);
21+
22+
std::vector<arg_t> vargs = {-1};
23+
h.fill(vargs); // CRASH, using h.fill(-1) or h.fill(args) does not crash.
24+
25+
return boost::report_errors();
26+
}

0 commit comments

Comments
 (0)