Python ctypes sprintf форматирует любой тип с плавающей точкой как b'0.000000 'или b'5.25662e-315' - PullRequest
1 голос
/ 15 апреля 2020

Я экспериментирую с самым быстрым способом форматирования числа с плавающей точкой в ​​виде строки с минимально возможным представлением (без конечных 0, без десятичных разрядов, если это возможно, без научной c записи). Я решил попробовать модуль Python ctypes.

Основываясь на нескольких примерах, я думал, что эта функция будет работать, но вместо этого она всегда печатает b'0.000000' при использовании %f или b'5.25124e-315' при использовании %g Код:

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, но я не знаю, что ответ является. Форматирование с использованием ints работает.

Использование Python 3.7.4 на Windows 10.

1 Ответ

2 голосов
/ 15 апреля 2020

Наблюдения:

  • Листинг [Python 3.Docs]: ctypes - библиотека сторонних функций для Python
  • Проверка [SO]: функция C, вызванная из Python через ctypes, возвращает неправильное значение (ответ @ CristiFati) при работе с CTypes функциями
  • [ Python 3.Docs]: встроенные типы - Numeri c типы - int, float, complex состояния ( выделение - мое):

    Floating числа точек обычно реализуются с использованием double в 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\StackOverflow\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 Шаблоны - Анекдот по оптимизации .

...