OR-Tools  9.6
rounding_modes_benchmark.cc
Go to the documentation of this file.
1 // Copyright 2010-2022 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 //
15 // Benchmarks for different implementations of interval arithmetic.
16 //
17 // The goal of this file is to see how the different possible implementations
18 // perform against one another, to get a ballpark estimate of the possible gain
19 // of a full hardware implementation of interval arithmetic as proposed
20 // in IEEE-1788.
21 //
22 // We tried to be as meaningful and simple as possible.
23 //
24 // For simpilicity, only addition is implemented, on a single double.
25 // We could make things faster, and amortize the cost of changing rounding
26 // modes.
27 // For example, it is possible, and it is often done, to represent
28 // intervals as a pair {min, -max} and only round towards -infinty.
29 // This improves things a bit, but far less than having instructions that
30 // do not need changing the rounding more.
31 // We could have computed the lower bounds of the sums, and then their upper
32 // bounds.
33 // However, these ideas do not work in the context of a generic bound
34 // propagator for arbitrarily complex formulas.
35 // Intervals have to be handled in a simple way, for the programmer to
36 // be able to use them, and most importantly the rounding mode must be
37 // reset to its default after each computation.
38 //
39 // AVX512 seems to be a promising direction, albeit incomplete, towards
40 // a good hardware implementation of interval computation.
41 // Note: compile with -mavx512f to get access to avx512.
42 //
43 // Interesting references:
44 // What every computer scientist should know about floating-point arithmetic
45 // ACM Computing Surveys Volume 23 Issue 1 March 1991 pp 5–48
46 // https://dl.acm.org/doi/10.1145/103162.103163
47 // also at https://docs.oracle.com/cd/E19957-01/800-7895/800-7895.pdf, with the
48 // addendum https://docs.oracle.com/cd/E37069_01/html/E39019/z400228248508.html.
49 //
50 // Accuracy and Stability of Numerical Algorithms by Nicholas J. Higham.
51 // https://epubs.siam.org/doi/book/10.1137/1.9780898718027
52 //
53 // The program outputs a CSV-formatted summary that is easily pastable in
54 // spreadsheets or table generators. ns/it is the number of nanoseconds per
55 // iteration.
56 //
57 // The correct value for SumOfIntegers is 500000000500000000.000000...
58 // The correct value for SumOfSquareRoots is 21081851083600.37596259382529338.
59 //
60 // Here is a sample run. Notice the width of the intervals. This is the price
61 // to pay for soundness. There exist algorithms to reduce them. See for example
62 // https://www.sciencedirect.com/science/article/pii/S0004370298000538
63 // and the related algorithms.
64 // The run was performed on a machine supporting AVX512F. AVX512F rounding masks
65 // per instruction make it possible to effectively use interval arithmetic, even
66 // though the instructions are not ideal.
67 //
68 // Name,ns/it,result
69 // SumOfIntegers<RoundToNearestEven>,1.1801, \
70 // [,500000000067108992.00000000000000,500000000067108992.00000000000000,]
71 // SumOfIntegers<StdRounding>,19.5461, \
72 // [,499999987755261568.00000000000000,500000013244738176.00000000000000,]
73 // SumOfIntegers<LowLevelRounding>,5.71184, \
74 // [,499999987755261568.00000000000000,500000013244738176.00000000000000,]
75 // SumOfIntegers<Avx512Rounding>,1.17857, \
76 // [,499999987755261568.00000000000000,500000013244738176.00000000000000,]
77 // SumOfSquareRoots<RoundToNearestEven>,2.7984, \
78 // [,21081851083600.55859375000000,21081851083600.55859375000000,]
79 // SumOfSquareRoots<StdRounding>,23.91, \
80 // [,21081850394046.03906250000000,21081851773099.68359375000000,]
81 // SumOfSquareRoots<LowLevelRounding>,6.64908, \
82 // [,21081850394046.03906250000000,21081851773099.68359375000000,]
83 // SumOfSquareRoots<Avx512Rounding>,2.91871, \
84 // [,21081850394046.03906250000000,21081851773099.68359375000000,]
85 
86 // TODO(user): make it work on ARM, RISC-V, and POWER.
87 
88 #include <cassert>
89 #include <cfenv> // NOLINT
90 #include <chrono> // NOLINT(build/c++11)
91 #include <cmath>
92 #include <cstdint>
93 #include <cstdio>
94 
95 #if defined(__x86_64__)
96 #include <x86intrin.h>
97 #endif
98 #if defined(_M_AMD64) // For MSVC.
99 #include <intrin.h>
100 #endif
101 #if defined(__x86_64__) || defined(_M_AMD64)
102 #define AMD64
103 #include <emmintrin.h>
104 #include <immintrin.h>
105 #include <xmmintrin.h>
106 #endif
107 
108 // Is this really necessary? g++ does not like it.
109 #if defined(__clang__)
110 #pragma STDC FENV_ACCESS ON
111 #endif
112 
114  // Abuse of language to describe an "interval" computed using the standard
115  // round-to-even mode.
116  static inline double AddUp(double a, double b) { return a + b; }
117  static inline double AddDown(double a, double b) { return a + b; }
118 };
119 
121  public:
122  explicit StdRoundingScope(int new_mode) : saved_mode_(std::fegetround()) {
123  std::fesetround(new_mode);
124  }
125  ~StdRoundingScope() { std::fesetround(saved_mode_); }
126 
127  private:
128  int saved_mode_;
129 };
130 
131 struct StdRounding {
132  // Simplest possible implementation of addition with rounding modes on a
133  // single double, using the standard fegetround() / fesetround()
134  // functions.
135  static inline double AddUp(double a, double b) {
136  StdRoundingScope up(FE_UPWARD);
137  return a + b;
138  }
139 
140  static inline double AddDown(double a, double b) {
141  StdRoundingScope down(FE_DOWNWARD);
142  return a + b;
143  }
144 };
145 
146 // Making it a little bit faster. Supported only on x86-64 for now.
147 // We use intrinsics to only touch the non-x87 FP rounding modes.
148 // Contrary to fesetround, it does nothing about x87 rounding mode.
149 // This is safe because we reset the status to what it was right after
150 // performing the operation. Also, we store the previous status, and
151 // therefore minimize the number of reads from the status register.
152 
153 // Returns the contents of the floating-point control register.
154 inline unsigned int GetFloatingPointControlRegister() {
155 #if defined(AMD64)
156  // Returns the contents of the MX control status register on x86.
157  return _mm_getcsr();
158 #else
159  // TODO(user): implement for other architectures.
160  return 0;
161 #endif // AMD64
162 }
163 
164 // Sets the contents of the floating-point control register.
165 inline void SetFloatingPointControlRegister(unsigned int status) {
166 #if defined(AMD64)
167  // Sets the contents of the MX control status register on x86.
168  _mm_setcsr(status);
169 #endif
170 }
171 
172 // Sets the rounding mode, using the constants as defined in <cfenv>.
173 inline void SetRoundingMode(int status, int mode) {
174 #if defined(AMD64)
175  // As said above, we're not touching the x87 part of the CPU.
176  constexpr int kRoundingModeMask = 0x6000;
177  constexpr int kRoundingModeShift = 3;
178  SetFloatingPointControlRegister((status & ~kRoundingModeMask) |
179  (mode << kRoundingModeShift));
180 #endif // AMD64
181 }
182 
184  public:
185  explicit LowLevelRoundingScope(int new_mode)
186  : saved_status_(GetFloatingPointControlRegister()) {
187  SetRoundingMode(saved_status_, new_mode);
188  }
190 
191  private:
192  int saved_status_;
193 };
194 
196  // Faster rounding addition using the above rounding mode functions.
197  static inline double AddUp(double a, double b) {
198  LowLevelRoundingScope up(FE_UPWARD);
199  return a + b;
200  }
201 
202  static inline double AddDown(double a, double b) {
203  LowLevelRoundingScope down(FE_DOWNWARD);
204  return a + b;
205  }
206 };
207 
208 #ifdef __AVX512F__
209 struct Avx512Rounding {
210  // Even better implementation of rounding addition using instructions
211  // for which one can specify the rounding mode explicitly.
212  // Using AVX512.
213  // Note: we did not manage to use multi-versioning with
214  // __attribute__((target("arch=skylake-avx512")))
215  // and a fallback version with __attribute__((target("default")))
216  // which should be done at the caller level and not here for performance
217  // reasons.
218  static inline double AddUp(double a, double b) {
219  const __m128d x = _mm_set_sd(a);
220  const __m128d y = _mm_set_sd(b);
221  const __m128d result =
222  _mm_add_round_sd(x, y, (_MM_FROUND_TO_POS_INF | _MM_FROUND_NO_EXC));
223  return _mm_cvtsd_f64(result);
224  }
225 
226  static inline double AddDown(double a, double b) {
227  const __m128d x = _mm_set_sd(a);
228  const __m128d y = _mm_set_sd(b);
229  const __m128d result =
230  _mm_add_round_sd(x, y, (_MM_FROUND_TO_NEG_INF | _MM_FROUND_NO_EXC));
231  return _mm_cvtsd_f64(result);
232  }
233 };
234 #else
236  static inline double AddUp(double a, double b) { return 0.0; }
237  static inline double AddDown(double a, double b) { return 0.0; }
238 };
239 #endif
240 
241 struct Interval {
242  Interval(double l, double u) : lb(l), ub(u) {}
243  double lb;
244  double ub;
245 };
246 
247 // The functions that we are considering return an interval, in which
248 // the correct answer is guaranteed to be.
249 typedef Interval (*IntervalFunction)(int n);
250 
251 // This is a small runner that calls an IntervalFunction, counts the
252 // nanoseconds in a portable way, counts the cycles, and reports various
253 // stats. We're not concerned with timing the instructions independently.
254 // Just reporting the number of cycles per iteration is good enough.
255 void Runner(IntervalFunction f, int n) {
256  std::chrono::steady_clock::time_point begin =
257  std::chrono::steady_clock::now();
258  // We don't use __rdtsc() because it's not portable, and it is influenced by
259  // throttling. I can't be used to give the true frequency of the CPU.
260  Interval result = f(n);
261  std::chrono::steady_clock::time_point end = std::chrono::steady_clock::now();
262  double time_in_nanos =
263  std::chrono::duration_cast<std::chrono::nanoseconds>(end - begin).count();
264  printf("%g,[,%20.14f,%20.14f,]\n", time_in_nanos / n, result.lb, result.ub);
265  fflush(stdout);
266 }
267 
268 // Compute the sum of the first natural numbers up to and including n.
269 // The result is n * (n + 1) / 2. For n = 1'000'000'000, it is
270 // 500'000'000'500'000'000 or 5.000000005e+17.
271 template <typename Impl>
273  const double max = n;
274  double l = 0.0;
275  double u = 0.0;
276  for (double d = 0.0; d <= max; d += 1.0) {
277  l = Impl::AddDown(l, d);
278  u = Impl::AddUp(u, d);
279  }
280  return Interval(l, u);
281 }
282 
283 // For 1'000'000'000, the default round-to-nearest rounding mode returns
284 // 500'000'000'067'108'992 with clang on an x86_64 PC running Linux, far
285 // from the actual result.
290 
291 // Compute the sum of the square roots of the first natural numbers up to
292 // and including n.
293 // It's a well-known case explained in "Floating-Point Computating: A Comedy
294 // of Errors?" by Gregory Tarzy and Neil Toda from Sun Microsytems on
295 // 2004-01-20 on the now-defunct site developers.sun.com.
296 // The standard rounding mode returns 21081851083600.55859375000000
297 // while the correct answer computed using Maxima is
298 // 21081851083600.37596259382529338.
299 // Interestingly, a Pentium II at 400MHz took more than 6 hours to complete.
300 // Different pairs of (OS, CPU) produced quite different results. Always
301 // wrong.
302 template <typename Impl>
304  Interval result(0.0, 0.0);
305  double l = 0.0;
306  double u = 0.0;
307  for (int i = 0; i <= n; ++i) {
308  const double d = std::sqrt(i);
309  l = Impl::AddDown(l, d);
310  u = Impl::AddUp(u, d);
311  }
312  return Interval(l, u);
313 }
314 
319 
320 #define BENCHMARK(fn, n) \
321  { \
322  printf("%s,", #fn); \
323  Runner(fn, n); \
324  }
325 
326 int main() {
327 #if !defined(AMD64)
328  printf("Warning: x86-64 instrinsics not supported.\n");
329 #endif
330 #if !defined(__AVX512F__)
331  printf("Warning: AVX512F not supported.\n");
332 #endif
333  const int n = 1'000'000'000;
334  printf("Name,ns/it,result\n");
343  return 0;
344 }
int64_t max
Definition: alldiff_cst.cc:140
int64_t b
int64_t a
absl::Status status
Definition: g_gurobi.cc:41
template Interval SumOfSquareRoots< RoundToNearestEven >(int)
template Interval SumOfIntegers< Avx512Rounding >(int)
Interval SumOfIntegers(int n)
void Runner(IntervalFunction f, int n)
template Interval SumOfIntegers< LowLevelRounding >(int)
template Interval SumOfIntegers< RoundToNearestEven >(int)
#define BENCHMARK(fn, n)
template Interval SumOfSquareRoots< StdRounding >(int)
template Interval SumOfSquareRoots< Avx512Rounding >(int)
Interval(* IntervalFunction)(int n)
template Interval SumOfIntegers< StdRounding >(int)
Interval SumOfSquareRoots(int n)
unsigned int GetFloatingPointControlRegister()
void SetRoundingMode(int status, int mode)
template Interval SumOfSquareRoots< LowLevelRounding >(int)
void SetFloatingPointControlRegister(unsigned int status)
std::optional< int64_t > end
static double AddDown(double a, double b)
static double AddUp(double a, double b)
Interval(double l, double u)
static double AddDown(double a, double b)
static double AddUp(double a, double b)
static double AddDown(double a, double b)
static double AddUp(double a, double b)
static double AddDown(double a, double b)
static double AddUp(double a, double b)