Allow custom multiplier in Benchmark::ThreadRange (#2304)

Currently Benchmark::ThreadRange hardcodes a geometric multiplier of 2 when generating thread counts across [min_threads, max_threads]. In contrast, DenseThreadRange allows a configurable stride parameter.

Add an optional mult parameter to Benchmark::ThreadRange (defaulting to 2) so users can specify geometric multipliers other than 2 (e.g. powers of 4 or 8) across thread ranges.

- Update ThreadRange in benchmark_api.h and benchmark_register.cc.

- Add unit tests in test/benchmark_test.cc and test/options_test.cc.

- Document the mult parameter and DenseThreadRange in docs/user_guide.md.

Closes #1531.

Co-authored-by: Roman Lebedev <lebedev.ri@gmail.com>
diff --git a/docs/user_guide.md b/docs/user_guide.md
index 5848b65..cd96baf 100644
--- a/docs/user_guide.md
+++ b/docs/user_guide.md
@@ -1218,14 +1218,28 @@
 ```
 
 To run the benchmark across a range of thread counts, instead of `Threads`, use
-`ThreadRange`. This takes two parameters (`min_threads` and `max_threads`) and
-runs the benchmark once for values in the inclusive range. For example:
+`ThreadRange`. This takes two parameters (`min_threads` and `max_threads`), and an
+optional geometric multiplier `mult` (defaulting to 2), and runs the benchmark once
+for values in the inclusive range. For example:
 
 ```c++
 BENCHMARK(BM_MultiThreaded)->ThreadRange(1, 8);
 ```
 
-will run `BM_MultiThreaded` with thread counts 1, 2, 4, and 8.
+will run `BM_MultiThreaded` with thread counts 1, 2, 4, and 8, while:
+
+```c++
+BENCHMARK(BM_MultiThreaded)->ThreadRange(1, 16, 4);
+```
+
+will run `BM_MultiThreaded` with thread counts 1, 4, and 16.
+
+You can also use `DenseThreadRange(min_threads, max_threads, stride = 1)` to step
+by an arithmetic stride:
+
+```c++
+BENCHMARK(BM_MultiThreaded)->DenseThreadRange(1, 4);
+```
 
 If the benchmarked code itself uses threads and you want to compare it to
 single-threaded code, you may want to use real-time ("wallclock") measurements
diff --git a/include/benchmark/benchmark_api.h b/include/benchmark/benchmark_api.h
index 1bff472..d7ba187 100644
--- a/include/benchmark/benchmark_api.h
+++ b/include/benchmark/benchmark_api.h
@@ -138,7 +138,7 @@
                                StatisticsFunc* statistics,
                                StatisticUnit unit = kTime);
   Benchmark* Threads(int t);
-  Benchmark* ThreadRange(int min_threads, int max_threads);
+  Benchmark* ThreadRange(int min_threads, int max_threads, int mult = 2);
   Benchmark* DenseThreadRange(int min_threads, int max_threads, int stride = 1);
   Benchmark* ThreadPerCpu();
   Benchmark* ThreadRunner(threadrunner_factory&& factory);
diff --git a/src/benchmark_register.cc b/src/benchmark_register.cc
index 560a762..97dee29 100644
--- a/src/benchmark_register.cc
+++ b/src/benchmark_register.cc
@@ -470,11 +470,12 @@
   return this;
 }
 
-Benchmark* Benchmark::ThreadRange(int min_threads, int max_threads) {
+Benchmark* Benchmark::ThreadRange(int min_threads, int max_threads, int mult) {
   BM_CHECK_GT(min_threads, 0);
   BM_CHECK_GE(max_threads, min_threads);
+  BM_CHECK_GE(mult, 2);
 
-  internal::AddRange(&thread_counts_, min_threads, max_threads, 2);
+  internal::AddRange(&thread_counts_, min_threads, max_threads, mult);
   return this;
 }
 
diff --git a/test/benchmark_test.cc b/test/benchmark_test.cc
index b98fbdf..0dd37bf 100644
--- a/test/benchmark_test.cc
+++ b/test/benchmark_test.cc
@@ -261,6 +261,29 @@
 BENCHMARK_TEMPLATE1_CAPTURE(BM_template1_capture, void, foo, 24UL);
 BENCHMARK_CAPTURE(BM_template1_capture<void>, foo, 24UL);
 
+void BM_ThreadRanges(benchmark::State& st) {
+  switch (st.range(0)) {
+    case 1:
+      assert(st.threads() == 1 || st.threads() == 2 || st.threads() == 4 ||
+             st.threads() == 8);
+      break;
+    case 2:
+      assert(st.threads() == 1 || st.threads() == 4 || st.threads() == 16);
+      break;
+    case 3:
+      assert(st.threads() == 2 || st.threads() == 4 || st.threads() == 16 ||
+             st.threads() == 32);
+      break;
+    default:
+      assert(false && "Invalid test case number");
+  }
+  while (st.KeepRunning()) {
+  }
+}
+BENCHMARK(BM_ThreadRanges)->Arg(1)->ThreadRange(1, 8);
+BENCHMARK(BM_ThreadRanges)->Arg(2)->ThreadRange(1, 16, 4);
+BENCHMARK(BM_ThreadRanges)->Arg(3)->ThreadRange(2, 32, 4);
+
 void BM_DenseThreadRanges(benchmark::State& st) {
   switch (st.range(0)) {
     case 1:
diff --git a/test/options_test.cc b/test/options_test.cc
index 7ace933..9056093 100644
--- a/test/options_test.cc
+++ b/test/options_test.cc
@@ -41,6 +41,7 @@
 BENCHMARK(BM_basic)->MinTime(0.1)->MinWarmUpTime(0.2);
 BENCHMARK(BM_basic)->UseRealTime();
 BENCHMARK(BM_basic)->ThreadRange(2, 4);
+BENCHMARK(BM_basic)->ThreadRange(1, 16, 4);
 BENCHMARK(BM_basic)->ThreadPerCpu();
 BENCHMARK(BM_basic)->Repetitions(3);
 BENCHMARK(BM_basic)