如何在Python和Perl中实现基本数据类型(字符串和整数)

时间:2011-06-15 21:10:56

标签: python perl

我最近一直想知道我对基本类型(如字符串和整数)执行的各种操作如何在性能方面起作用,我想如果我知道这些基本类型是如何实现的话,我可以更好地理解这一点(即我听说字符串和整数在Python中是不可变的。这是否意味着修改字符串中一个字符的任何操作都是O(n)因为必须创建一个全新的字符串?如何添加数字?)

我对Python和Perl都很好奇,并且感到愚蠢地问两次基本相同的问题,所以我只是把它包装成一个。

如果您可以在答案中加入一些示例运营成本,那将会更有帮助。

2 个答案:

答案 0 :(得分:6)

在python中,some_string[5] = 'a'将是一个错误,但最接近的等效操作some_string = some_string[5:] + 'a' + some_string[6:]确实是O(n)。但这不仅仅是不可变对象的真实情况。连接列表也是如此:[1,2,3] + [4,5,6]生成一个新列表并且是O(n)。

添加数字会创建一个新值,但通常结果值在内存中的大小始终相同,因此它是O(1)。当然,这只适用于小额。一旦达到某个阈值(我的机器上有20位数字),突然注入会占用不同的空间。我不知道这种效果如何渐近表现。

然而,我发现它甚至在log10(n) == 1000附近甚至没有显着影响:

>>> times = [timeit.timeit(stmt=stmt.format(10 ** i, 10 ** i), number=100) for i in range(1000)]
>>> sum(times) * 1.0 / len(times)
3.0851364135742186e-06
>>> times[-1]
3.0994415283203125e-06

对于字符串,渐近性能命中更明显:

>>> stmt = 's[:5] + "a" + s[6:]'
>>> setup = 's = "b" * {0}'
>>> times = [timeit.timeit(stmt=stmt, setup=setup.format(i), number=10) for i in range(100000)]
>>> sum(times) * 1.0 / len(times)
6.2434492111206052e-05
>>> times[-1]
0.0001220703125

上一次操作的执行时间远低于平均值。趋势非常稳定:

>>> for t in times[0:100000:10000]:
...     print t
... 
5.00679016113e-06
1.31130218506e-05
2.90870666504e-05
3.88622283936e-05
5.10215759277e-05
6.19888305664e-05
7.41481781006e-05
8.48770141602e-05
9.60826873779e-05
0.000108957290649

但是,像小字符串这样的操作相当便宜。


要扩展您的其他问题,索引访问在列表和字符串上都是O(1)。

>>> stmt = 'x = s[{0}] + s[{1}] + s[{2}]'
>>> setup = 's = "a" * {0}'
>>> times = [timeit.timeit(stmt=stmt.format(i / 2, i / 3, i / 4), setup=setup.format(i + 1), number=10) for i in range(1000000)]
>>> sum(times) * 1.0 / len(times)
3.6441037654876707e-06
>>> times[-1]
3.0994415283203125e-06

与列表相同:

>>> stmt = 'x = s[{0}] + s[{1}] + s[{2}]'
>>> setup = 's = ["a"] * {0}'
>>> times = [timeit.timeit(stmt=stmt.format(i / 2, i / 3, i / 4), setup=setup.format(i + 1), number=10) for i in range(100000)]
>>> sum(times) * 1.0 / len(times)
2.8617620468139648e-06
>>> times[-1]
1.9073486328125e-06

切片复制字符串列表,因此O(n)带有n == len(slice)。没有“好”的方法来替换一个字符串的一个字母,虽然我想强调大多数时候“坏”的方式已经足够好了。如果你想要一个“好”的方式,使用不同的数据类型;操作列表并在需要字符串时加入它;或使用StringIO对象。 This page提供了一些关于连接不同内置Python数据类型的有用信息。

最后,既然你真的对内部感兴趣,我在stringobject.h中挖出了struct PyStringObject的声明(来自版本2.7; 3+可能看起来不同)。这是关于你期望的 - 一个带有一些额外花里胡哨的c弦:

typedef struct {
    PyObject_VAR_HEAD

PyObject_VAR_HEAD是一个c预处理器宏,根据解释的here规则扩展为类似下面的内容。)

    Py_ssize_t ob_refcnt;
    PyTypeObject *ob_type;
    Py_ssize_t ob_size;

...继续

    long ob_shash;
    int ob_sstate;
    char ob_sval[1];

    /* Invariants:
     *     ob_sval contains space for 'ob_size+1' elements.
     *     ob_sval[ob_size] == 0.
     *     ob_shash is the hash of the string or -1 if not computed yet.
     *     ob_sstate != 0 iff the string object is in stringobject.c's
     *       'interned' dictionary; in this case the two references
     *       from 'interned' to this object are *not counted* in ob_refcnt.
     */
} PyStringObject;

列表中有一个similar structure - c数组,带有额外的铃声和口哨声 - 但不是空终止,通常有额外的预分配存储空间。

毋庸置疑......这很多只适用 到cPython - PyPyIronPythonJython可能看起来完全不同!

答案 1 :(得分:6)

Perl字符串肯定不是不可变的。每个字符串都有一个缓冲区,缓冲区中字符串的初始偏移量,缓冲区的长度以及使用的缓冲区数量。此外,对于utf8字符串,在需要计算字符长度时会缓存字符长度。有一点,也有一些额外的字符偏移缓存到字节偏移信息,但我不确定它是否仍然存在。

如果需要增加缓冲区,则重新分配它。许多平台上的Perl都知道系统malloc的粒度,所以它可以为一个11字节的字符串分配一个14字节的缓冲区,知道它实际上不会占用任何额外的内存。

初始偏移量允许O(1)从字符串的开头删除数据。