Google OR-Tools v9.11
a fast and portable software suite for combinatorial optimization
Loading...
Searching...
No Matches
vector_sum_internal.h
Go to the documentation of this file.
1// Copyright 2010-2024 Google LLC
2// Licensed under the Apache License, Version 2.0 (the "License");
3// you may not use this file except in compliance with the License.
4// You may obtain a copy of the License at
5//
6// http://www.apache.org/licenses/LICENSE-2.0
7//
8// Unless required by applicable law or agreed to in writing, software
9// distributed under the License is distributed on an "AS IS" BASIS,
10// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11// See the License for the specific language governing permissions and
12// limitations under the License.
13
14#ifndef OR_TOOLS_UTIL_VECTOR_SUM_INTERNAL_H_
15#define OR_TOOLS_UTIL_VECTOR_SUM_INTERNAL_H_
16
17#include <algorithm>
18#include <cstddef>
19#include <cstdint>
20#include <iterator>
21#include <numeric>
22
23#include "absl/base/attributes.h"
24#include "absl/base/optimization.h"
25#include "absl/types/span.h"
27
28namespace operations_research {
29namespace internal {
30
31// A contiguous block of memory that contains `size` values of type `Value`, and
32// the first value is aligned to `alignment` bytes.
33template <typename Value, size_t size, size_t alignment = sizeof(Value) * size>
34struct alignas(alignment) AlignedBlock {
35 Value values[size] = {};
36
37 Value Sum() const {
38 alignas(alignment) Value sum[size];
39 std::copy(std::begin(values), std::end(values), std::begin(sum));
40 for (int i = size; i > 1; i /= 2) {
41 int middle = i / 2;
42 for (int j = 0; j < middle; ++j) {
43 sum[j] += sum[middle + j];
44 }
45 }
46 return sum[0];
47 }
48};
49
50// In-place addition for two AlignedBlock values. Adds `in` to `out`, storing
51// the value in `out`. Unless something steps in, this compiles into a single
52// `*addp*` instruction.
53template <typename Value, size_t size>
55 const AlignedBlock<Value, size>& in) {
56 for (int i = 0; i < size; ++i) {
57 out.values[i] += in.values[i];
58 }
59}
60
61// Computes the sum of `num_blocks` aligned blocks. Proceeds in three phases:
62// 1. Parallel sum with N = `num_blocks_per_iteration` independent block-sized
63// accumulators. At the end, accumulator i contains partial sums at indices
64// i, i + N, i + 2*N, ..., i + M*N, where M is the largest number of blocks
65// such that i+M*N < num_blocks for all i.
66// 2. Parallel addition of remaining blocks. All remaining blocks are added to
67// accumulators 0, ..., num_remaining_blocks - 1.
68// 3. Sum of accumulators: all accumulators are added together. The result is a
69// single block-sized accumulator that is returned to the caller.
70//
71// The code of the function was specifically tuned for 32-bit floating point
72// values, and works best with block_size = 4 and num_blocks_per_iteration = 4.
73// It will likely work with other types, but may require extra care and possibly
74// different parameters.
75//
76// NOTE(user): As of 2023-04-28, Clang's auto-vectorizer is *very* brittle
77// and most attempts to reduce the last accumulator from step 3 into a single
78// value prevents the rest of the function from being vectorized. To mitigate
79// this behavior we return the whole block (which should normally fit into a
80// single vector register). We also mark the function with
81// `ABSL_ATTRIBUTE_NOINLINE` to make prevent the inliner from merging this
82// function with any additional code that would prevent vectorization.
83template <size_t block_size, size_t num_blocks_per_iteration, typename Value>
85 const AlignedBlock<Value, block_size>* blocks, size_t num_blocks) {
87 static_assert(sizeof(Block[2]) == sizeof(Block::values) * 2,
88 "The values in the block are not packed.");
89
90 AlignedBlock<Value, block_size> sum[num_blocks_per_iteration];
91
92 const int leftover_blocks = num_blocks % num_blocks_per_iteration;
93 const int packed_blocks = num_blocks - leftover_blocks;
94
95 const AlignedBlock<Value, block_size>* aligned_block_end =
96 blocks + packed_blocks;
97
98 // Phase 1: Parallel sum of the bulk of the data.
99 if (packed_blocks >= num_blocks_per_iteration) {
100 std::copy(blocks, blocks + num_blocks_per_iteration, sum);
101 }
102 for (int i = num_blocks_per_iteration; i < packed_blocks;
103 i += num_blocks_per_iteration) {
104 for (int j = 0; j < num_blocks_per_iteration; ++j) {
105 AddInPlace(sum[j], blocks[i + j]);
106 }
107 }
108
109 // Phase 2: Semi-parallel sum of the remaining up to
110 // num_blocks_per_iteration - 1 blocks.
111 for (int i = 0; i < leftover_blocks; ++i) {
112 AddInPlace(sum[i], aligned_block_end[i]);
113 }
114
115 // Phase 3: Reduce the accumulator blocks to a single block.
116 // NOTE(user): When this code is auto-vectorized correctly, the initial
117 // copy is a no-op, and the for loop below translates to
118 // num_blocks_per_iteration - 1 vector add instructions. In 2023-05, I
119 // experimented with other versions (e.g. using sum[0] as the target or making
120 // res a const reference to sum[0], but in most cases they broke vectorization
121 // of the whole function).
123 for (int i = 1; i < num_blocks_per_iteration; ++i) {
124 AddInPlace(res, sum[i]);
125 }
126
127 return res;
128}
129
130// Computes the sum of values in `values`, by adding `num_blocks_per_iteration`
131// blocks of `block_size` values.
132// By default, the sum does not make any assumptions about the size or alignment
133// of `values`. When the first item of `values` is known to be aligned to
134// `block_size * sizeof(Value)` bytes, `assume_aligned_at_start` can be used to
135// save a small amount of time.
136template <size_t block_size = 4, size_t num_blocks_per_iteration = 4,
137 bool assume_aligned_at_start = false, typename Value>
138Value VectorSum(absl::Span<const Value> values) {
140 const Value* start_ptr = values.data();
141 const int size = values.size();
142 // With less than two blocks, there's not a lot to vectorize, and a simple
143 // sequential sum is usually faster.
144 if (size == 0) return Value{0};
145 if (size < 2 * block_size) {
146 return std::accumulate(start_ptr + 1, start_ptr + size, *start_ptr);
147 }
148
149 if (assume_aligned_at_start) {
150 ABSL_ASSUME(reinterpret_cast<std::uintptr_t>(start_ptr) % alignof(Block) ==
151 0);
152 }
153 const Value* aligned_start_ptr =
154 assume_aligned_at_start ? start_ptr : AlignUp<alignof(Block)>(start_ptr);
155 const Block* blocks = reinterpret_cast<const Block*>(aligned_start_ptr);
156 const Value* end_ptr = start_ptr + size;
157 const Value* aligned_end_ptr = AlignDown<alignof(Block)>(end_ptr);
158 ABSL_ASSUME(aligned_start_ptr <= aligned_end_ptr);
159 const size_t num_blocks = (aligned_end_ptr - aligned_start_ptr) / block_size;
160 ABSL_ASSUME(
161 reinterpret_cast<std::uintptr_t>(aligned_end_ptr) % alignof(Block) == 0);
162
163 Value leading_items_sum{};
164 if (!assume_aligned_at_start) {
165 leading_items_sum = std::accumulate(start_ptr, aligned_start_ptr, Value{});
166 }
167 Block res =
168 AlignedBlockSum<block_size, num_blocks_per_iteration>(blocks, num_blocks);
169 Value block_sum = res.Sum();
170 return std::accumulate(aligned_end_ptr, end_ptr,
171 block_sum + leading_items_sum);
172}
173
174} // namespace internal
175} // namespace operations_research
176
177#endif // OR_TOOLS_UTIL_VECTOR_SUM_INTERNAL_H_
IntegerValue size
void AddInPlace(AlignedBlock< Value, size > &out, const AlignedBlock< Value, size > &in)
AlignedBlock< Value, block_size > ABSL_ATTRIBUTE_NOINLINE AlignedBlockSum(const AlignedBlock< Value, block_size > *blocks, size_t num_blocks)
In SWIG mode, we don't want anything besides these top-level includes.
float VectorSum(absl::Span< const float > values)
Computes the sum of values without assuming anything.
Definition vector_sum.h:37