[libc-commits] [libc] [libc] Fix subnormal performance in strtod/strtof (PR #218991)
Michael Hoisie via libc-commits
libc-commits at lists.llvm.org
Wed Aug 26 10:51:01 PDT 2026
https://github.com/hoisie updated https://github.com/llvm/llvm-project/pull/218991
>From d92aaa04dd73b93d3fcc52e38ac8c2a4367c5425 Mon Sep 17 00:00:00 2001
From: Michael Hoisie <hoisie at gmail.com>
Date: Wed, 26 Aug 2026 09:27:45 -0700
Subject: [PATCH] [libc] Fix subnormal performance in strtod/strtof
This makes parsing subnormal doubles around 12x faster and subnormal floats
around 4x faster.
Previously, calls like strtod("5e-324") took around ~64us, vs ~11ns for a
normal value like 1e-300. The main cause was that MAX_SHIFT_AMOUNT was 4, when
it should have been 60, as indicated by the code comments. This means that
there were 15x more shifts than intended. Another issue is that the subnormal
branch was shifting one bit at a time instead of taking advantage of the
chunking already present in shift.
The behavior of strtod and strtof are unchanged.
+------------------------------+------------+-----------+---------+
| Benchmark | Before | After | Speedup |
+------------------------------+------------+-----------+---------+
| BM_StrToDoubleNormalShort | 59.4 ns | 59.9 ns | 1.0x |
| BM_StrToDoubleNormalLong | 973.0 ns | 362.0 ns | 2.7x |
| BM_StrToDoubleSubnormalShort | 350,611 ns | 28,624 ns | 12.2x |
| BM_StrToDoubleSubnormalLong | 152,181 ns | 11,865 ns | 12.8x |
| BM_StrToFloatNormalShort | 57.9 ns | 58.2 ns | 1.0x |
| BM_StrToFloatSubnormalShort | 48.8 ns | 49.6 ns | 1.0x |
| BM_StrToFloatSubnormal | 9,535 ns | 2,173 ns | 4.4x |
+------------------------------+------------+-----------+---------+
---
libc/benchmarks/CMakeLists.txt | 17 ++++
.../LibcStrToFloatGoogleBenchmarkMain.cpp | 82 +++++++++++++++++++
libc/src/__support/high_precision_decimal.h | 3 +-
libc/src/__support/str_to_float.h | 8 +-
4 files changed, 104 insertions(+), 6 deletions(-)
create mode 100644 libc/benchmarks/LibcStrToFloatGoogleBenchmarkMain.cpp
diff --git a/libc/benchmarks/CMakeLists.txt b/libc/benchmarks/CMakeLists.txt
index fe49377a01986..2c91e9bd901b6 100644
--- a/libc/benchmarks/CMakeLists.txt
+++ b/libc/benchmarks/CMakeLists.txt
@@ -261,6 +261,23 @@ target_link_libraries(libc.benchmarks.memory_functions.opt_host
llvm_update_compile_flags(libc.benchmarks.memory_functions.opt_host)
add_dependencies(libc.benchmarks.memory_functions.opt_host google-benchmark-libc)
+add_executable(libc.benchmarks.str_to_float.opt_host
+ EXCLUDE_FROM_ALL
+ LibcStrToFloatGoogleBenchmarkMain.cpp
+)
+target_include_directories(libc.benchmarks.str_to_float.opt_host
+ PRIVATE
+ ${LIBC_SOURCE_DIR}
+ ${LIBC_INCLUDE_DIR}
+)
+target_link_libraries(libc.benchmarks.str_to_float.opt_host
+ PRIVATE
+ benchmark_main
+ libc-benchmark
+ libc.src.errno.errno.__internal__
+)
+llvm_update_compile_flags(libc.benchmarks.str_to_float.opt_host)
+
if(LIBC_TYPES_HAS_FLOAT16)
add_executable(libc.benchmarks.rsqrtf16.opt_host
EXCLUDE_FROM_ALL
diff --git a/libc/benchmarks/LibcStrToFloatGoogleBenchmarkMain.cpp b/libc/benchmarks/LibcStrToFloatGoogleBenchmarkMain.cpp
new file mode 100644
index 0000000000000..6c106b30cab61
--- /dev/null
+++ b/libc/benchmarks/LibcStrToFloatGoogleBenchmarkMain.cpp
@@ -0,0 +1,82 @@
+//===-- String to float benchmarks ------------------------------*- C++ -*-===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+#include "benchmark/benchmark.h"
+#include "src/__support/str_to_float.h"
+
+#include <stddef.h>
+#include <stdint.h>
+
+namespace {
+
+constexpr const char *NORMAL_SHORT[] = {
+ "0", "1", "1.5", "-3.75", "0.1", "12.5", "3.14159", "100", "1e10", "1e-10",
+};
+
+constexpr const char *NORMAL_LONG[] = {
+ "1.00000000000000000001", "0.99999999999999999999",
+ "123456789012345678901234567890", "1.7976931348623157e308",
+ "2.2250738585072014e-308",
+};
+
+constexpr const char *SUBNORMAL_SHORT[] = {
+ "5e-324", "1e-320", "2.5e-320", "1e-315", "3.33e-318", "7e-309",
+};
+
+constexpr const char *SUBNORMAL_LONG[] = {
+ "4.940656458412465441765687928682213723651e-324",
+ "1.797693134862315708145274237317043567981e-320",
+};
+
+// Float subnormals: below ~1.18e-38, bottoming out at 1.4e-45.
+constexpr const char *FLOAT_SUBNORMAL[] = {
+ "1e-38", "5e-39", "1e-40", "1e-42", "1e-44", "1.4012984643e-45",
+};
+
+template <typename T, size_t N>
+void run(benchmark::State &state, const char *const (&inputs)[N]) {
+ for (auto _ : state) {
+ for (const char *input : inputs)
+ benchmark::DoNotOptimize(
+ LIBC_NAMESPACE::internal::strtofloatingpoint<T>(input));
+ }
+ state.SetItemsProcessed(state.iterations() * N);
+}
+
+void BM_StrToDoubleNormalShort(benchmark::State &state) {
+ run<double>(state, NORMAL_SHORT);
+}
+void BM_StrToDoubleNormalLong(benchmark::State &state) {
+ run<double>(state, NORMAL_LONG);
+}
+void BM_StrToDoubleSubnormalShort(benchmark::State &state) {
+ run<double>(state, SUBNORMAL_SHORT);
+}
+void BM_StrToDoubleSubnormalLong(benchmark::State &state) {
+ run<double>(state, SUBNORMAL_LONG);
+}
+
+void BM_StrToFloatNormalShort(benchmark::State &state) {
+ run<float>(state, NORMAL_SHORT);
+}
+void BM_StrToFloatSubnormalShort(benchmark::State &state) {
+ run<float>(state, SUBNORMAL_SHORT);
+}
+void BM_StrToFloatSubnormal(benchmark::State &state) {
+ run<float>(state, FLOAT_SUBNORMAL);
+}
+
+} // namespace
+
+BENCHMARK(BM_StrToDoubleNormalShort);
+BENCHMARK(BM_StrToDoubleNormalLong);
+BENCHMARK(BM_StrToDoubleSubnormalShort);
+BENCHMARK(BM_StrToDoubleSubnormalLong);
+BENCHMARK(BM_StrToFloatNormalShort);
+BENCHMARK(BM_StrToFloatSubnormalShort);
+BENCHMARK(BM_StrToFloatSubnormal);
diff --git a/libc/src/__support/high_precision_decimal.h b/libc/src/__support/high_precision_decimal.h
index de22172fd8d3e..b9f3d90f5ca5c 100644
--- a/libc/src/__support/high_precision_decimal.h
+++ b/libc/src/__support/high_precision_decimal.h
@@ -135,7 +135,8 @@ class HighPrecisionDecimal {
// The maximum amount we can shift is the number of bits used in the
// accumulator, minus the number of bits needed to represent the base (in this
// case 4).
- static constexpr uint32_t MAX_SHIFT_AMOUNT = sizeof(uint64_t) - 4;
+ static constexpr uint32_t MAX_SHIFT_AMOUNT =
+ cpp::numeric_limits<uint64_t>::digits - 4;
// 800 is an arbitrary number of digits, but should be
// large enough for any practical number.
diff --git a/libc/src/__support/str_to_float.h b/libc/src/__support/str_to_float.h
index ace2e130a5e19..47a97f2a5eedd 100644
--- a/libc/src/__support/str_to_float.h
+++ b/libc/src/__support/str_to_float.h
@@ -418,11 +418,9 @@ LIBC_INLINE FloatConvertReturn<T> simple_decimal_conversion(
// Handle subnormals
if (exp2 <= 0) {
- // Shift right until there is a valid exponent
- while (exp2 < 0) {
- hpd.shift(-1);
- ++exp2;
- }
+ // Shift right until there is a valid exponent.
+ hpd.shift(exp2);
+ exp2 = 0;
// Shift right one more time to compensate for the left shift to get it
// between 1 and 2.
hpd.shift(-1);
More information about the libc-commits
mailing list