Python ctypes 的 sprintf 将任何浮点类型格式化为 b'0.000000' 或 b'5.25662e-315'

Python ctypes's sprintf formats any float type as b'0.000000' or b'5.25662e-315'

我正在尝试以最快的方式将浮点数格式化为具有尽可能少的表示形式的字符串(没有尾随 0,没有小数位,如果可以的话,没有科学记数法)。我决定试试 Python 的 ctypes 模块。

根据几个例子,我认为这个函数可以工作,但如果使用 %f,它总是打印 b'0.000000',如果使用 %g,它总是打印 b'5.25124e-315' 代码:

from ctypes import *
import msvcrt
def floatToStr3(n:float)->str:
    libc = cdll.msvcrt
    print("n in:", n)
    sb = create_string_buffer(100)
    libc.sprintf(sb, b"%g", c_float(n))
    print("sb out:", sb.value)
    return sb.value

import random
floatToStr3(random.random())
floatToStr3(random.random())
floatToStr3(random.random())
floatToStr3(random.random())
floatToStr3(random.random())
floatToStr3(random.random())

输出:

n in: 0.9164215022054657
sb out: b'5.25662e-315'
n in: 0.6366531536720886
sb out: b'5.23343e-315'
n in: 0.07371310207853521
sb out: b'5.1052e-315'
n in: 0.6353450576077702
sb out: b'5.23332e-315'
n in: 0.2839487624658935
sb out: b'5.18628e-315'
n in: 0.5540225836869241
sb out: b'5.22658e-315'

我有一种强烈的感觉,我只是没有正确使用 create_string_buffer,但我不知道答案是什么。使用整数进行格式化。

在 Windows 10.

上使用 Python 3.7.4

观察:

  • 列表[Python 3.Docs]: ctypes - A foreign function library for Python
  • 在使用 CTypes 函数时检查
  • [Python 3.Docs]: Built-in Types - Numeric Types - int, float, complex 声明(强调 是我的):

    Floating point numbers are usually implemented using double in C

    通过将数字转换为 ctypes.c_float它会失去精度(通常是 float 是 4 个字节长,而 double 是 8 个字节),产生的值非常接近 0 ,因此输出(也由@frost-nzcr4直觉)

  • 直接调用sprintf,绝对比调用任何其他Python转换函数要快。但是我们不要忘记 Python 有很多优化,所以即使函数调用本身更快,调用所需的开销也是可能的( Python <=>C 转换)可能更高,在某些情况下整体性能比使用 Python解决方案
  • 如果我们谈论速度,将 sb = create_string_buffer(100)(和其他) 放在函数 中并不是很明智。在外面做(一次,在开始时)并且只在函数中使用它

下面是一个例子。

code00.py:

#!/usr/bin/env python

import sys
import ctypes as ct
import timeit
import random


c_float = ct.c_float
c_double = ct.c_double
cdll = ct.cdll
create_string_buffer = ct.create_string_buffer


swprintf = ct.windll.msvcrt.swprintf
swprintf.argtypes = [ct.c_wchar_p, ct.c_wchar_p, ct.c_double]  # !!! swprintf (and all the family functions) have varargs !!!
swprintf.restype = ct.c_int
buf = ct.create_unicode_buffer(100)


def original(f: float) -> str:
    libc_ = cdll.msvcrt
    #print("n in:", f)
    sb = create_string_buffer(100)
    libc_.sprintf(sb, b"%g", c_double(f))
    #print("sb out:", sb.value)
    return sb.value.decode()


def improved(f: float) -> str:
    swprintf(buf, "%g", f)
    return buf.value


def percent(f: float) -> str:
    return "%g" % f


def format(f: float) -> str:
    return "{0:g}".format(f)


def f_string(f: float) -> str:
    return f"{f}"


number_count = 3
numbers = [random.random() for _ in range(number_count)]
number = numbers[0]


def main(*argv):
    funcs = [
        original,
        improved,
        percent,
        format,
        f_string,
    ]

    print("Functional tests")
    for f in numbers:
        print("\nNumber (default format): {0:}".format(f))
        for func in funcs:
            print("    {0:s}: {1:}".format(func.__name__, func(f)))

    print("\nPerformance tests (time took by each function)")
    for func in funcs:
        t = timeit.timeit(stmt="func(number)", setup="from __main__ import number, {0:s} as func".format(func.__name__))
        print("    {0:s}: {1:}".format(func.__name__, t))


if __name__ == "__main__":
    print("Python {0:s} {1:d}bit on {2:s}\n".format(" ".join(item.strip() for item in sys.version.split("\n")), 64 if sys.maxsize > 0x100000000 else 32, sys.platform))
    main(*sys.argv[1:])
    print("\nDone.")

输出:

[cfati@CFATI-5510-0:e:\Work\Dev\Whosebug\q061231308]> "e:\Work\Dev\VEnvs\py_pc064_03.07.06_test0\Scripts\python.exe" code00.py
Python 3.7.6 (tags/v3.7.6:43364a7ae0, Dec 19 2019, 00:42:30) [MSC v.1916 64 bit (AMD64)] 64bit on win32

Functional tests

Number (default format): 0.7598920818033322
    original: 0.759892
    improved: 0.759892
    percent: 0.759892
    format: 0.759892
    f_string: 0.7598920818033322

Number (default format): 0.985689825577911
    original: 0.98569
    improved: 0.98569
    percent: 0.98569
    format: 0.98569
    f_string: 0.985689825577911

Number (default format): 0.613914001222863
    original: 0.613914
    improved: 0.613914
    percent: 0.613914
    format: 0.613914
    f_string: 0.613914001222863

Performance tests (time took by each function)
    original: 2.324927
    improved: 1.8772565999999995
    percent: 0.3631088
    format: 0.5225973999999995
    f_string: 1.2965244999999994

Done.

正如所见,内置 Python 替代方案的性能优于 CTypes 替代方案。我感到好奇的是(想知道我是否没有做错什么),f-string 变体要低得多(性能方面) 比我预期的要好。
阅读 [Python]: Python Patterns - An Optimization Anecdote 可能会很有趣。