Адрес интернета и памяти для str и Py_UNICODE в Cython - PullRequest
1 голос
/ 17 мая 2019

Контекст: я построил древовидную структуру данных, которая хранит отдельные символы в своих узлах в Cython. Теперь мне интересно, смогу ли я сохранить память, если я интернирую всех этих персонажей. И стоит ли мне использовать Py_UNICODE в качестве типа переменной или обычного str. Это мой урезанный объект Node, использующий Py_UNICODE:

from libc.stdint cimport uintptr_t
from cpython cimport PyObject

cdef class Node():
    cdef:
        public Py_UNICODE character

    def __init__(self, Py_UNICODE character):
        self.character = character

    def memory(self):
        return <uintptr_t>&self.character

Если сначала попытаться увидеть, были ли персонажи интернированы автоматически. Если я импортирую этот класс в Python и создаю несколько объектов с разными или одинаковыми символами, я получу следующие результаты:

a = Node("a")
a_py = a.character
a2 = Node("a")
b = Node("b")

print(a.memory(), a2.memory(), b.memory())
# 140532544296704 140532548558776 140532544296488

print(id(a.character), id(a2.character), id(b.character), id(a_py))
# 140532923573504 140532923573504 140532923840528 140532923573504

Таким образом, из этого я бы сделал вывод, что Py_UNICODE не интернируется автоматически, и что использование id () в python не дает мне фактический адрес памяти, а адрес копии (я полагаю, что python автоматически вводит отдельные символы Юникода, а затем просто возвращает мне адрес памяти).

Затем я попытался сделать то же самое, когда использовал str. Простая замена Py_UNICODE на str не сработала , поэтому я пытаюсь сделать это сейчас:

%%cython
from libc.stdint cimport uintptr_t
from cpython cimport PyObject

cdef class Node():
    cdef:
        public str character

    def __init__(self, str character):
        self.character = character

    def memory(self):
        return <uintptr_t>(<PyObject*>self.character)

И вот результаты, которые я получаю с этим:

...
print(a.memory(), a2.memory(), b.memory())
# 140532923573504 140532923573504 140532923840528

print(id(a.character), id(a2.character), id(b.character), id(a_py))
# 140532923573504 140532923573504 140532923840528 140532923573504

Исходя из этого, я сначала подумал, что односимвольные строки также интернированы в cython, и что просто не нужно копировать символы из python, чтобы объяснить, почему id () и .memory () дают один и тот же адрес. Но затем я попытался использовать более длинные строки, получил те же результаты, из которых я, вероятно, не хочу делать вывод, что более длинные строки также автоматически интернируются? Это также тот случай, когда мое дерево использует меньше памяти, если я использую Py_UNICODE, так что не имеет особого смысла, если str были интернированы, а Py_UNICODE - нет. Может кто-нибудь объяснить мне это поведение? А как мне пройти стажировку?

(я тестирую это в Jupyter, на случай, если что-то изменится)

Редактировать: удалено ненужное сравнение идентификаторов узлов вместо символов.

1 Ответ

2 голосов
/ 17 мая 2019

Есть недоразумение с вашей стороны.PY_UNICODE это не объект Python - это typedef для wchar_t.

Только интернированные строковые объекты (по крайней мере, некоторые из них), но не простые C-переменные типа wchar_t (или фактически любого C-типа).Это также не имеет никакого смысла: wchar_t, скорее всего, имеет размер 32 бита, в то время как указатель на интернированный объект будет стоить 64 бита.

Таким образом, адрес памяти переменной self.character (типа PY_UNICODE) никогда не бывает одинаковым до тех пор, пока self являются различными объектами (независимо от того, какое значение имеет self.character).

С другой стороны, когда вы вызываете a.character в чистом Python, Cython знает, что переменная не является простым 32-битным целым числом и автоматически преобразует его (character это свойство правильно?) В unicode-объект через PyUnicode_FromOrdinal.Возвращенная строка (т. Е. a_py) может быть «интернирована» или нет.

Когда кодовая точка этого символа меньше 256 (т. Е. Latin1), он получает вид интернированного - иначе нет.Первые 256 unicode-объектов, состоящих только из одного символа, имеют специальное место - не , как другие интернированные строки (таким образом, использование "interned" в предыдущем разделе).

Рассмотрим:

>>> a="\u00ff" # ord(a) =  255
>>> b="\u00ff"
>>> a is b
# True

, но

>>> a="\u0100" # ord(a) =  256
>>> b="\u0100"
>>> a is b
# False

Ключ от этого: используйте PY_UNICODE - даже дешевле (4 байта)если не интернировано, чем интернированные строки / юникод-объекты (8 байт для справки + один раз память для интернированного объекта) и намного дешевле, чем не интернированные объекты (что может случиться).*, как указал @ user2357112, используйте Py_UCS4, чтобы гарантировать гарантированный размер 4 байта (который необходим для поддержки всех возможных символов Юникода) - wchar_t может бытьвсего 1 байт (даже если это, вероятно, довольно необычно в наши дни).Если вы знаете больше об используемых символах, вы можете вернуться к Py_UCS2 или Py_UCS1.


Однако при использовании Py_UCS2 или Py_USC1 нужно учитывать, что Cythonне будет поддерживать преобразование из / в Unicode, как в случае Py_UCS4 (или устарел Py_UNICODE), и нужно будет сделать это вручную, например:

%%cython 
from libc.stdint cimport uint16_t

# need to wrap typedef as Cython doesn't do it
cdef extern from "Python.h":
    ctypedef uint16_t Py_UCS2

cdef class Node:
    cdef:
        Py_UCS2 character_

    @property
    def character(self):
        # cython will do the right thing for Py_USC4
        return <Py_UCS4>(self.character_) 

    def __init__(self, str character):
        # unicode -> Py_UCS4 managed by Cython
        # Py_UCS4 -> Py_UCS2 is a simple C-cast
        self.character_ = <Py_UCS2><Py_UCS4>(character)

Также следует сделатьУбедитесь, что использование Py_USC2 действительно экономит память: CPython использует pymalloc , который имеет выравнивание 8 байтов, это означает, что объект, например, 20 байтов, все еще будет использовать 24 байта (3 * 8) памяти.Другая проблема - выравнивание для структур, исходящих от C-компилятора, поскольку

struct A{
    long long int a;
    long long int b;
    char ch;
};

sizeof(A) равно 24 вместо 17 (см. live ).

Если одинна самом деле после этих двух байтов есть более крупная рыба, которую нужно жарить: не делайте ноды Python-объектами, поскольку это приводит к накладным расходам в 16 байтов для не очень необходимого полиморфизма и подсчета ссылок - это означает, что вся структура данных должна быть записанаС и завернуть в целом в Python.Однако и здесь обязательно убедитесь, что память выделена правильно: обычные распределители памяти C-runtime имеют выравнивание 32 или 64 байта, т.е. выделение меньших размеров все еще приводит к использованию 32/64 байта.

...