From dccbf8b1daaa0159cd1e15f4d1b0c65af6df1310 Mon Sep 17 00:00:00 2001 From: Filip Leonarski Date: Sat, 17 Oct 2026 12:42:00 +0200 Subject: [PATCH] Reflection files: the mmCIF and SHELX numbers formatted with std::to_chars Writing the merged reflections - p.mtz, p.cif, p.hkl, ~250 MB for 4nwv - takes 5.3 s on the Windows laptop against 0.46 s on Linux. The mmCIF rows (twelve "%.4f" fields each) and the HKLF 4 records (two "%.2f") are formatted with snprintf, ~50 M calls for 4nwv; the likely Windows cost is the MSVC runtime's printf (slower float formatting, locale handling per call) - not measured there yet. Fmt and the HKLF 4 number now go through std::to_chars(fixed, precision) where the standard library has it for floating point (__cpp_lib_to_chars: libstdc++ 11+, MSVC); libc++ before macOS 13.3 does not, and keeps snprintf. The same digits: both round the exact binary value correctly, halfway cases to even. Checked against glibc on 20 M float-valued doubles at 4 and 2 decimals and on 8 M exact binary fractions (every tie), 0 differences. On this machine (gcc-toolset-13) 52 against 194 ns a field. p.hkl, the MTZ data and p.cif (apart from its provenance lines) byte-identical. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01PXc1zD1yxHqaR1DGdCTHL4 --- image_analysis/WriteReflections.cpp | 31 +++++++++++++++++++---------- 1 file changed, 20 insertions(+), 11 deletions(-) diff --git a/image_analysis/WriteReflections.cpp b/image_analysis/WriteReflections.cpp index faf3b5075..22bfb3e1c 100644 --- a/image_analysis/WriteReflections.cpp +++ b/image_analysis/WriteReflections.cpp @@ -9,6 +9,7 @@ #include "../common/ParallelFor.h" #include +#include #include #include #include @@ -45,16 +46,28 @@ std::string CurrentDateISO() { return buf; } +/// The C locale's "%.*f" of a finite double. std::to_chars where the standard library has it for +/// floating point: the same digits - both round the exact binary value, halfway cases to even - at a +/// quarter of snprintf's time, and with no locale to consult. snprintf elsewhere (libc++ before macOS +/// 13.3). 512 characters hold any finite double. +std::string FixedDigits(double val, int decimals) { + char buf[512]; +#ifdef __cpp_lib_to_chars + const auto r = std::to_chars(buf, buf + sizeof(buf), val, std::chars_format::fixed, decimals); + return std::string(buf, r.ptr); +#else + const int n = std::snprintf(buf, sizeof(buf), "%.*f", decimals, val); + return std::string(buf, n); +#endif +} + /// Format a double with given decimal places; returns "?" for non-finite. -/// snprintf rather than an ostringstream: the reflection loop below calls this twelve times per -/// reflection, and building a stream (and its locale) per call dominated the time spent writing a -/// merged file. Same digits - both go through the C locale's %.*f. +/// Not an ostringstream: the reflection loop below calls this twelve times per reflection, and +/// building a stream (and its locale) per call dominated the time spent writing a merged file. std::string Fmt(double val, int decimals = 4) { if (!std::isfinite(val)) return "?"; - char buf[512]; - const int n = std::snprintf(buf, sizeof(buf), "%.*f", decimals, val); - return std::string(buf, n); + return FixedDigits(val, decimals); } /// Quote a CIF string value; returns "?" for empty. @@ -691,11 +704,7 @@ namespace { if (v.size() < width) s.append(width - v.size(), ' '); s.append(v); }; - const auto num2 = [](double v) { - char b[64]; - const int n = std::snprintf(b, sizeof b, "%.2f", v); - return std::string(b, static_cast(std::clamp(n, 0, static_cast(sizeof b) - 1))); - }; + const auto num2 = [](double v) { return FixedDigits(v, 2); }; column(std::to_string(h), 4); column(std::to_string(k), 4); column(std::to_string(l), 4);