diff --git a/CMakeLists.txt b/CMakeLists.txt index 650a233..b5145d3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -22,6 +22,7 @@ option(SUPPORT_MSVC_STYLE_INTEGER_SPECIFIERS "Support the I + bit size integer option(SUPPORT_WRITEBACK_SPECIFIER "Support the length write-back specifier (%n)" ON) option(SUPPORT_LONG_LONG "Support long long integral types (allows for the ll length modifier and affects %p)" ON) option(USE_DOUBLE_INTERNALLY "Use the C `double` type - typically 64-bit in size - for internal floating-point arithmetic " ON) +option(AVOID_INT64_TO_FLOAT_CONVERSION "Avoid int64_t-to-float conversions when using single-precision internal floating-point arithmetic" OFF) option(CHECK_FOR_NUL_IN_FORMAT_SPECIFIER "Be defensive in the undefined-behavior case of a format specifier not ending before the string ends" ON) set(ALIASING_MODES NONE HARD SOFT) @@ -41,6 +42,7 @@ foreach(opt SUPPORT_WRITEBACK_SPECIFIER SUPPORT_LONG_LONG USE_DOUBLE_INTERNALLY + AVOID_INT64_TO_FLOAT_CONVERSION ALIAS_STANDARD_FUNCTION_NAMES_SOFT ALIAS_STANDARD_FUNCTION_NAMES_HARD CHECK_FOR_NUL_IN_FORMAT_SPECIFIER diff --git a/README.md b/README.md index 70d4bfc..b669857 100644 --- a/README.md +++ b/README.md @@ -102,6 +102,7 @@ Options used both in CMake and in the library source code via a preprocessor def | SUPPORT_WRITEBACK_SPECIFIER | YES | Support the length write-back specifier (%n) | | SUPPORT_LONG_LONG | YES | Support long long integral types (allows for the ll length modifier and affects %p) | | USE_DOUBLE_INTERNALLY | YES | Use the `double` for internal floating-point calculations (rather than using the single-precision `float` type | +| AVOID_INT64_TO_FLOAT_CONVERSION | NO | When using single-precision internal floating-point calculations, avoid int64_t-to-float conversions by using smaller fixed-point chunks with lower maximum precision | In the source files themselves, these options become preprocessor definitions with a `PRINTF_` prefix; the boolean ones have value `1` OR `0` for YES or NO respectively; and the aliasing option translates into one of three definitions: `ALIAS_STANDARD_FUNCTION_NAMES_NONE`, `ALIAS_STANDARD_FUNCTION_NAMES_SOFT` or `ALIAS_STANDARD_FUNCTION_NAMES_HARD`. @@ -294,4 +295,3 @@ I try to attend to issues and PRs promptly. ## License This library is published under the terms of the [MIT license](http://www.opensource.org/licenses/MIT). - diff --git a/printf_config.h.in b/printf_config.h.in index ab47f76..7734a03 100644 --- a/printf_config.h.in +++ b/printf_config.h.in @@ -7,6 +7,7 @@ #define PRINTF_SUPPORT_MSVC_STYLE_INTEGER_SPECIFIERS @PRINTF_SUPPORT_MSVC_STYLE_INTEGER_SPECIFIERS@ #define PRINTF_SUPPORT_LONG_LONG @PRINTF_SUPPORT_LONG_LONG@ #define PRINTF_USE_DOUBLE_INTERNALLY @PRINTF_USE_DOUBLE_INTERNALLY@ +#define PRINTF_AVOID_INT64_TO_FLOAT_CONVERSION @PRINTF_AVOID_INT64_TO_FLOAT_CONVERSION@ #ifdef PRINTF_ALIAS_STANDARD_FUNCTION_NAMES_SOFT #if (PRINTF_ALIAS_STANDARD_FUNCTION_NAMES_SOFT != @PRINTF_ALIAS_STANDARD_FUNCTION_NAMES_SOFT@) @@ -32,4 +33,3 @@ #define PRINTF_CHECK_FOR_NUL_IN_FORMAT_SPECIFIER @PRINTF_CHECK_FOR_NUL_IN_FORMAT_SPECIFIER@ #endif /* PRINTF_CONFIG_H_ */ - diff --git a/src/printf/printf.c b/src/printf/printf.c index 824e469..34cf680 100644 --- a/src/printf/printf.c +++ b/src/printf/printf.c @@ -117,6 +117,15 @@ #define PRINTF_USE_DOUBLE_INTERNALLY 1 #endif +/* + * Use 32-bit integer chunks for single-precision floating point formatting. This avoids + * int64_t-to-float conversions on targets requiring large and slow software float routines, at the + * cost of lower maximum precision. + */ +#ifndef PRINTF_AVOID_INT64_TO_FLOAT_CONVERSION +#define PRINTF_AVOID_INT64_TO_FLOAT_CONVERSION 0 +#endif + /* * According to the C languages standard, printf() and related functions must be able to print any * integral number in floating-point notation, regardless of length, when using the %f specifier - @@ -280,15 +289,37 @@ typedef unsigned int printf_size_t; * representation and manipulation of values as long doubles; the options * are either single-precision `float` or double-precision `double`. */ +#define NUM_DECIMAL_DIGITS_IN_INT64_T 18 +#define NUM_DECIMAL_DIGITS_IN_INT32_T 10 + +/* + * Keep the fixed-point decimal chunks within the integer type used to hold + * them. The value is a precision, so it is one less than the number of base-10 + * digits the type can safely hold for values in [0, 10^precision). + */ #if PRINTF_USE_DOUBLE_INTERNALLY typedef double floating_point_t; +typedef int_fast64_t fp_integral_component_t; #define FP_TYPE_MANT_DIG DBL_MANT_DIG +#define NUM_DECIMAL_DIGITS_IN_FP_INTEGRAL_COMPONENT_T NUM_DECIMAL_DIGITS_IN_INT64_T #else typedef float floating_point_t; +#if PRINTF_AVOID_INT64_TO_FLOAT_CONVERSION +typedef int32_t fp_integral_component_t; +#define NUM_DECIMAL_DIGITS_IN_FP_INTEGRAL_COMPONENT_T NUM_DECIMAL_DIGITS_IN_INT32_T +#else +typedef int_fast64_t fp_integral_component_t; +#define NUM_DECIMAL_DIGITS_IN_FP_INTEGRAL_COMPONENT_T NUM_DECIMAL_DIGITS_IN_INT64_T +#endif #define FP_TYPE_MANT_DIG FLT_MANT_DIG #endif -#define NUM_DECIMAL_DIGITS_IN_FP_INTEGRAL_COMPONENT_T 18 +/* + * Note: This value does not mean that all floating-point values printed with the + * library will be correct up to this precision; it is just an upper-bound for + * avoiding buffer overruns and such. + */ +#define PRINTF_MAX_SUPPORTED_PRECISION (NUM_DECIMAL_DIGITS_IN_FP_INTEGRAL_COMPONENT_T - 1) #if FP_TYPE_MANT_DIG == 24 @@ -648,8 +679,6 @@ static void print_integer(output_gadget_t* output, printf_unsigned_value_t value #if (PRINTF_SUPPORT_DECIMAL_SPECIFIERS || PRINTF_SUPPORT_EXPONENTIAL_SPECIFIERS) -typedef int_fast64_t fp_integral_component_t; - /* * Stores a fixed-precision representation of a floating-point number relative * to a fixed precision (which cannot be determined by examining this structure) @@ -671,14 +700,6 @@ static const floating_point_t powers_of_10[PRINTF_MAX_PRECOMPUTED_POWER_OF_10 + #endif }; -/* - * Note: This value does not mean that all floating-point values printed with the - * library will be correct up to this precision; it is just an upper-bound for - * avoiding buffer overruns and such - */ -#define PRINTF_MAX_SUPPORTED_PRECISION (NUM_DECIMAL_DIGITS_IN_FP_INTEGRAL_COMPONENT_T - 1) - - /* * Break up a non-negative, finite, floating-point number into two integral * parts of its decimal representation: The number up to the decimal point, @@ -1173,6 +1194,17 @@ static void print_floating_point(output_gadget_t* output, floating_point_t value return; } +#if (!PRINTF_USE_DOUBLE_INTERNALLY && PRINTF_AVOID_INT64_TO_FLOAT_CONVERSION) + /* + * The decimal-to-exponential fallback below happens before the common precision-limiting block. + * Keep the requested precision within the 32-bit fractional chunk too. + */ + while ((len < PRINTF_DECIMAL_BUFFER_SIZE) && (precision > PRINTF_MAX_SUPPORTED_PRECISION)) { + buf[len++] = '0'; /* This respects the precision in terms of result length only */ + precision--; + } +#endif + if (!prefer_exponential && ((value > PRINTF_FLOAT_NOTATION_THRESHOLD) || (value < -PRINTF_FLOAT_NOTATION_THRESHOLD))) { /* @@ -1660,4 +1692,3 @@ int fctprintf(void (*out)(char c, void* extra_arg), void* extra_arg, const char* va_end(args); return ret; } - diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index b365e57..fce4c76 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -25,6 +25,21 @@ foreach(tgt ${test_targets}) endif() endforeach() +add_executable(avoid_int64_to_float_conversion "avoid_int64_to_float_conversion.cpp") +set_target_properties( + avoid_int64_to_float_conversion + PROPERTIES + CXX_STANDARD 11 + CXX_STANDARD_REQUIRED YES + CXX_EXTENSIONS NO +) +target_include_directories( + avoid_int64_to_float_conversion + PRIVATE + "$" +) +list(APPEND test_targets avoid_int64_to_float_conversion) + add_executable(aliasing "aliasing.c") set_target_properties( aliasing @@ -110,5 +125,9 @@ add_test( target_link_libraries(aliasing PRIVATE printf) target_include_directories(aliasing PRIVATE "$") -# Not running autotest by default - it's randomized after all. +add_test( + NAME "${PROJECT_NAME}.avoid_int64_to_float_conversion" + COMMAND "avoid_int64_to_float_conversion" +) +# Not running autotest by default - it's randomized after all. diff --git a/test/avoid_int64_to_float_conversion.cpp b/test/avoid_int64_to_float_conversion.cpp new file mode 100644 index 0000000..435de51 --- /dev/null +++ b/test/avoid_int64_to_float_conversion.cpp @@ -0,0 +1,33 @@ +#define PRINTF_VISIBILITY static +#define PRINTF_USE_DOUBLE_INTERNALLY 0 +#define PRINTF_AVOID_INT64_TO_FLOAT_CONVERSION 1 +#define PRINTF_SUPPORT_EXPONENTIAL_SPECIFIERS 0 +#define PRINTF_SUPPORT_WRITEBACK_SPECIFIER 0 + +#include + +static void putchar_(char c) +{ + (void) c; +} + +#define CATCH_CONFIG_MAIN +#include "catch.hpp" + +#include +#include + +TEST_CASE("single precision can avoid int64 to float conversions") +{ + char buffer[64]; + + STATIC_REQUIRE(sizeof(((struct floating_point_components*) 0)->integral) == sizeof(int32_t)); + STATIC_REQUIRE(sizeof(((struct floating_point_components*) 0)->fractional) == sizeof(int32_t)); + STATIC_REQUIRE(PRINTF_MAX_SUPPORTED_PRECISION == 9); + + sprintf_(buffer, "%.10f", 1.25); + CHECK(std::strcmp(buffer, "1.2500000000") == 0); + + sprintf_(buffer, "%.18f", -67224.546875); + CHECK(std::strcmp(buffer, "-67224.546875008000000000") == 0); +}