`<algorithm>`: `ranges::min`, `ranges::max` and `ranges::minmax` evaluate each element twice
Nobody has claimed this yet.
- Dominant language
- C++
- Stars
- 11.1k
- Forks
- 1.7k
- Avg merge
- 4d 15h
- Merged PRs (30d)
- 22
Description
Describe the bug
std::ranges::min/max/minmax over a views::transform range invoke the transform roughly twice per element. libstdc++ invokes it once, and libc++'s count depends on the data. Since these overloads return range_value_t<R> by value and are constrained on indirectly_copyable_storable, one evaluation per element appears to be permitted.
Command-line test case
// cl /EHsc /std:c++latest /O2 repro.cpp
#include <algorithm>
#include <cstdio>
#include <ranges>
#include <vector>
int calls = 0;
struct Entry { double v; double get() const { ++calls; return v; } };
int main() {
std::vector<Entry> data;
for (int i = 1; i <= 8; ++i) data.push_back(Entry{static_cast<double>(i)});
calls = 0;
(void) std::ranges::min(data | std::views::transform(&Entry::get));
std::printf("ranges::min : %d\n", calls); // 15
calls = 0;
double m = 1e300;
for (const auto& e : data) m = (std::min)(m, e.get());
std::printf("hand loop : %d\n", calls); // 8
}
Observed (N = 8, so 8 is one call per element and 15 is 2N−1):
| MS STL 19.51 | libstdc++ | libc++ | |
|---|---|---|---|
| ranges::min over transform_view | 15 | 8 | <=15 |
| ranges::max over transform_view | 15 | 8 | <=15 |
| ranges::minmax over transform_view | 22 | 8 | 22 |
| ranges::fold_left over transform_view | 8 | 8 | 8 |
| hand-written loop | 8 | 8 | 8 |
Expected behavior
One evaluation per element, as libstdc++ achieves.
STL version
Microsoft Visual Studio 18 (Insiders), toolset 14.51.36231, compiler 19.51.36252, x64.
Additional context
[alg.min.max]/7 constrains comparisons and applications of the projection:
Complexity: Exactly
ranges::distance(r) - 1comparisons and twice as many applications of the projection, if any.
Here the projection is identity, and I could not find any wording in [alg.min.max] or [algorithms.requirements] that constrains how many times *i may be evaluated. An implementation that stores range_value_t<R> rather than tracking an iterator would satisfy the stated complexity exactly while halving the dereferences. minmax seems to be even more permissive with its "at most" wording.
Note that [alg.min.max]/27 has similar wording on the min_element variant, except that "if any" is suspiciously missing:
Complexity: Exactly
max(last - first - 1,0)comparisons and twice as many projections.
I guess the pedantic reading of this gives the reason why min_element cannot have a similar optimisation (along with minor performance and design quirks on caching reference alongside of the iterator). This is deliberately not part of this report.
Measured impact
I was trying to write a blog post advocating for C++20 ranges, and ran into unexpected performance issues when using costly transform.
With a non-trivial transform (std::cos(angle) * length), MSVC 19.51 /O2: 2.09× a hand-written loop at N = 131072, and 4.77× at N = 2048. MSVC is not able to optimise out the redundancy at /O2 /Ob3 /GL /arch:AVX2 /LTCG or with /fp:fast.
If the maintainers consider this a worthwhile change, I would be happy to prepare a pull request for std::max/min/minmax with identity projection.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start by locating the ranges::min, ranges::max, and ranges::minmax implementations and their tests, then run the supplied repro.cpp command-line case to measure transform calls. Done means each transform element is evaluated once for these overloads while their stated comparison and projection complexity remains satisfied.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- cpp
- Domain
- performance
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 55/100