关于python:Python随笔1int对象池

32次阅读

共计 7104 个字符,预计需要花费 18 分钟才能阅读完成。

0. 前言

浙江省新版高中技术教材将采纳 Python 3 作为信息技术教学语言。作为一名高一学生,笔者开始温习本人的 Python 常识。温习之余,特意开设这个系列,来记录本人的温习所得。

本次笔记中提到的问题由笔者的一位同学提出,与 Python 中的 int object pool 无关。

1. 问题形容

代码片段如下:

var_a = 1
var_b = 1
print("Address: var_a: {0} var_b: {1}".format(id(var_a), id(var_b)))
print("var_a is var_b? {0}".format(var_a is var_b))

var_c = 300
var_d = 300
print("Address: var_c: {0} var_d: {1}".format(id(var_c), id(var_d)))
print("var_c is var_d? {0}".format(var_c is var_d))

在 Python shell 中执行产生如下后果:

>>> 
>>> var_a = 1
>>> var_b = 1
>>> print("Address: var_a: {0} var_b: {1}".format(id(var_a), id(var_b)))
Address: var_a: 9310336 var_b: 9310336
>>> print("var_a is var_b? {0}".format(var_a is var_b))
var_a is var_b? True
>>> 
>>> var_c = 300
>>> var_d = 300
>>> print("Address: var_c: {0} var_d: {1}".format(id(var_c), id(var_d)))
Address: var_c: 140399450822160 var_d: 140399450823472
>>> print("var_c is var_d? {0}".format(var_c is var_d))
var_c is var_d? False
>>>                                                                                 

为什么同样是 int 类型的值,在对象地址和 is 判断中会有差异?

2. 解决方案与探讨

Python 3 的 C 语言实现(又称 CPython,即 python.org 上提供下载的版本)中,存在着 int 对象缓存池,即 int object pool。为了进步解释器的运行效率,CPython 会默认在解释器初始化时创立好一小部分(最罕用的)int 对象,存储在一个数组中。当这些 int 被援用时,解释器便间接返回曾经缓存好的对象地址,而无需重新分配。这点在 CPython 的代码中能够清晰地看到。

// Python-3.8.5/Objects/longobject.c, L18-L23
#ifndef NSMALLPOSINTS
#define NSMALLPOSINTS           257
#endif
#ifndef NSMALLNEGINTS
#define NSMALLNEGINTS           5
#endif

下面这段代码中,CPython 预约义了两个宏,别离定义了“较小数字”的正边界(NSMALLPOSINTS)和“较小数字”的负边界(NSMALLNEGINTS)。也就是说,CPython 中的 int object pool 边界在此定义。

// Python-3.8.5/Objects/longobject.c, L37-L43

#if NSMALLNEGINTS + NSMALLPOSINTS > 0
/* Small integers are preallocated in this array so that they
   can be shared.
   The integers that are preallocated are those in the range
   -NSMALLNEGINTS (inclusive) to NSMALLPOSINTS (not inclusive).
*/
static PyLongObject small_ints[NSMALLNEGINTS + NSMALLPOSINTS];

这段代码创立了一个 PyLongObject 的数组。这个数组中能够放下在[NSMALLNEGINTS, NSMALLPOSINTS)(左闭右开)之内的数。

// Python-3.8.5/Objects/longobject.c, L48-L79

static PyObject *
get_small_int(sdigit ival)
{
    PyObject *v;
    assert(-NSMALLNEGINTS <= ival && ival < NSMALLPOSINTS);
    v = (PyObject *)&small_ints[ival + NSMALLNEGINTS];
    Py_INCREF(v);
#ifdef COUNT_ALLOCS
    if (ival >= 0)
        _Py_quick_int_allocs++;
    else
        _Py_quick_neg_int_allocs++;
#endif
    return v;
}
#define CHECK_SMALL_INT(ival) \
    do if (-NSMALLNEGINTS <= ival && ival < NSMALLPOSINTS) { \
        return get_small_int((sdigit)ival); \
    } while(0)

static PyLongObject *
maybe_small_long(PyLongObject *v)
{if (v && Py_ABS(Py_SIZE(v)) <= 1) {sdigit ival = MEDIUM_VALUE(v);
        if (-NSMALLNEGINTS <= ival && ival < NSMALLPOSINTS) {    // <=== 关注这行
            Py_DECREF(v);
            return (PyLongObject *)get_small_int(ival);
        }
    }
    return v;
}

这个函数实现了判断数字是否在缓存范畴内并获取缓存地址的性能,其中标有标记的一行便是要害的判断语句。该语句实现了对上述范畴的判断。

思考如下两段代码:

# code/python_int_pool_a.py


for x in range(10000, 20000):
    for y in range(0, 100):
        pass
# code/python_int_pool_b.py


for x in range(10000, 20000):
    for y in range(300, 400):
        pass

咱们应用 shell 内置命令 time 统计两段代码的执行工夫,后果如下:

$ time python ./python_int_pool_a.py

real    0m0.099s
user    0m0.000s
sys     0m0.015s

$ time python ./python_int_pool_b.py

real    0m0.115s
user    0m0.000s
sys     0m0.031s

$

显著,同样调配 100 个 int,可能应用int object poolpython_int_pool_a.py(0.099s)远快于间接调配的python_int_pool_b.py(0.115s)。常量池的劣势便在这里体现进去。

3. 延长

一般来说,咱们对于 Python 代码进行运行工夫剖析,有以下三种办法:

  1. time工具

最简略的计时办法,个别在想要获取解释器总体运行工夫时有较好体现。

长处: 简洁直白;内置于 shell 中;额定开销少;对任意可执行文件皆无效

毛病: 输入过于简略,没有具体执行信息;shell 之间实现不统一

样例:

$ time python ./python_int_pool_a.py 

real    0m0.071s
user    0m0.046s
sys        0m0.009s

$ zsh
% time python ./python_int_pool_a.py 
python ./python_int_pool_b.py  0.04s user 0.00s system 99% cpu 0.047 total

%

对于它的应用不再赘述。

  1. cProfile模块

CPython 自带的性能统计工具,能够准确到每个函数。

长处: 使用方便;内置于 Python 中;准确到每个 Python 函数

毛病: 开销较大;输入繁冗;只针对 Python 脚本无效;只存在于 CPython 中

样例:

$ python -m cProfile ./python_int_pool_a.py 
         3 function calls in 0.022 seconds

   Ordered by: standard name

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
        1    0.022    0.022    0.022    0.022 python_int_pool_a.py:4(<module>)
        1    0.000    0.000    0.022    0.022 {built-in method builtins.exec}
        1    0.000    0.000    0.000    0.000 {method 'disable' of '_lsprof.Profiler' objects}


$ 

能够看到,输入内容十分具体,蕴含了函数调用次数 ncalls,函数本体(不包含子函数调用)运行总工夫tottime,函数(包含子函数调用)运行总工夫cumtime,具体的文件名、行号和函数名。然而对于较大的脚本,cProfile 便显得过于具体以至于繁杂了:

$ python -m cProfile gen_pattern.py --help
    ... 省略脚本自身的输入 ...

         24601 function calls (23801 primitive calls) in 0.024 seconds

   Ordered by: standard name

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
       37    0.000    0.000    0.000    0.000 <frozen importlib._bootstrap>:1009(_handle_fromlist)
       28    0.000    0.000    0.000    0.000 <frozen importlib._bootstrap>:103(release)
       28    0.000    0.000    0.000    0.000 <frozen importlib._bootstrap>:143(__init__)
       28    0.000    0.000    0.000    0.000 <frozen importlib._bootstrap>:147(__enter__)
       28    0.000    0.000    0.000    0.000 <frozen importlib._bootstrap>:151(__exit__)
       28    0.000    0.000    0.000    0.000 <frozen importlib._bootstrap>:157(_get_module_lock)
       28    0.000    0.000    0.000    0.000 <frozen importlib._bootstrap>:176(cb)
     36/2    0.000    0.000    0.019    0.009 <frozen importlib._bootstrap>:211(_call_with_frames_removed)
      319    0.000    0.000    0.000    0.000 <frozen importlib._bootstrap>:222(_verbose_message)
        8    0.000    0.000    0.000    0.000 <frozen importlib._bootstrap>:232(_requires_builtin_wrapper)
       25    0.000    0.000    0.000    0.000 <frozen importlib._bootstrap>:307(__init__)
       25    0.000    0.000    0.000    0.000 <frozen importlib._bootstrap>:311(__enter__)
       25    0.000    0.000    0.000    0.000 <frozen importlib._bootstrap>:318(__exit__)
      100    0.000    0.000    0.000    0.000 <frozen importlib._bootstrap>:321(<genexpr>)
       16    0.000    0.000    0.000    0.000 <frozen importlib._bootstrap>:35(_new_module)
       26    0.000    0.000    0.000    0.000 <frozen importlib._bootstrap>:369(__init__)
       33    0.000    0.000    0.000    0.000 <frozen importlib._bootstrap>:403(cached)
       25    0.000    0.000    0.000    0.000 <frozen importlib._bootstrap>:416(parent)
       25    0.000    0.000    0.000    0.000 <frozen importlib._bootstrap>:424(has_location)
        8    0.000    0.000    0.000    0.000 <frozen importlib._bootstrap>:433(spec_from_loader)
       25    0.000    0.000    0.000    0.000 <frozen importlib._bootstrap>:504(_init_module_attrs)
       25    0.000    0.000    0.002    0.000 <frozen importlib._bootstrap>:576(module_from_spec)
       28    0.000    0.000    0.000    0.000 <frozen importlib._bootstrap>:58(__init__)
     25/2    0.000    0.000    0.020    0.010 <frozen importlib._bootstrap>:663(_load_unlocked)
       26    0.000    0.000    0.000    0.000 <frozen importlib._bootstrap>:719(find_spec)
        8    0.000    0.000    0.000    0.000 <frozen importlib._bootstrap>:740(create_module)
        
        ... 此处省略 529 行 ...

        1    0.000    0.000    0.000    0.000 {method 'write' of '_io.TextIOWrapper' objects}


$

此处 gen_pattern.py 出自Open Source Computer Vision Libraryopencv/opencv),作者 Jaycee(yassiezar),Vladislav Sovrasov(sovrasov),S. Garrido(sergarrido),Nicholas Nadeau(nnadeau),Rong “Mantle” Bao(CSharperMantle)和 debjan(debjan)

  1. 采纳 ld 链接器的运行时库打桩

应用运行时库打桩机制,咱们能够重定向任意函数至咱们本人的函数。

长处: 高度自定义;自由度高

毛病: 只在带有 GCC 的 Linux 设施上起效;应用简单

样例:

这里咱们打桩分配内存的规范库函数malloc()

// code/mymalloc.cpp

#include <cstdio>
#include <dlfcn.h>
#include <stdexcept>

#define LOG_INFO std::printf

#ifdef __cplusplus
extern "C" {
#endif /* __cplusplus */

void* malloc(size_t size)
{auto symbol_addr = dlsym(RTLD_NEXT, "malloc");
    if (!symbol_addr)
    {throw std::runtime_error(dlerror());
    }
    typedef void* (*__malloc)(size_t);
    auto libc_malloc = reinterpret_cast<__malloc>(symbol_addr);
    auto ptr = libc_malloc(size);

    static thread_local bool called = false;
    if (!called)
    {
        called = true;
        LOG_INFO("malloc(%ld) = %p\n", size, ptr);
        called = false;
    }

    return ptr;
}

void free(void *ptr)
{auto symbol_addr = dlsym(RTLD_NEXT, "free");
    if (!symbol_addr)
    {throw std::runtime_error(dlerror());
    }
    typedef void (*__free)(void *);
    auto libc_free = reinterpret_cast<__free>(symbol_addr);
    libc_free(ptr);
    LOG_INFO("free() = %p\n", ptr);
}

#ifdef __cplusplus
}
#endif /* __cplusplus */

编译、运行:

$ g++ -o mymalloc.so mymalloc.cpp -shared -fPIC -ldl -D_GNU_SOURCE -g -Wall -Wextra

$ LD_PRELOAD="./mymalloc.so" python ./python_int_pool_a.py 
malloc(72704) = 0x138a2d0
malloc(32) = 0x139c2f0
malloc(2) = 0x139c320
malloc(5) = 0x139c340
free() = 0x139c340
malloc(120) = 0x139c360

... 以下省略...

$

THE END

正文完
 0