Python高性能编程¶
理解高性能Python¶
计算单元¶
CPU是常见的计算单元
最初被设计用于加速计算机图像处理的图形处理单元(GPU)现在变得更加适用于数值计算, 因为其本身的并行模式使大量计算能并发进行
计算单元的主要属性是其每个周期能进行的操作数量以及每秒能完成多少个周期
第一个属性 通过每周期完成的指令数(IPC)来衡量 第二个属性 通过其时钟频率来衡量
Intel的Core系列具有非常高的IPC, 但时钟速度都很低
GPU的IPC和始终速度都很高, 但它们由别的问题
时钟速度提高时, 能够立即提高该计算单元上的所有程序运行速度(每秒可以进行更多的运算)
提高IPC则在矢量计算能力上很大的影响
矢量计算是指一次提供多个数据给一个CPU并能同时被操作., 这种类型的CPU指令被称为SIMD(单指令多数据)
如今就是那单元进展有限, 所以芯片制造商通过其他手段获取更高的速度
超线程技术 乱序执行 多核架构
CPU增加更多核心并不一定能提升程序运行的速度, 这是有 阿姆达尔定律决定的
阿姆达尔定律: 如果一个可以运行在多核上的程序有某种执行路径必须在单核上运行, 那么这些路径就会成为瓶颈导致最终速度无法通过增加更多核心来提高
任何并行计算的瓶颈最终都会落在其顺序执行的那部分任务上.
对于Python来说, 充分利用多核心能的阻碍主要在于Python的全局解释器锁(GIL)
GIL确保Python进程一次只能执行一条指令
存储单元¶
寄存器, RAM, 硬盘 主要区别在于读写数据的速度 而读写数据的速度 与 数据的读写方式相关
大多数存储单元一次读一大块数据的性能远好于读多次小块数据( 顺序读取 VS 随机读取 )
所有存储单元或多或少的受到这一影响, 但不同类型存储单元收到的影响却大不相同
存储单位还有一个延时的属性
读写速度由低到高排序
旋转硬盘: 计算机关机也能保持长期存储, 读写速度慢, 磁盘必须物理旋转和等待磁头移动. 随机访问新更能下降但容量高(TB几倍)
固态硬盘: 读写速度快但容量较小(GB级别) RAM: 用于保存应用程序的代码和数据(比如用到的各种变量), 具有更快的读写速度且在随机访问时性能良好, 受限于容量
L1/L2/L3: 极快的读写速度, 进入CPU必须经过这里. 容量很小
通信层¶
总线 前端总线是 RAM和L1/L2缓存之间的连接. 总线的主要属性是它的速度: 在给定时间内它能传输多少数据., 由两个因素决定: 一次能传多少数据(总线带宽), 和每秒能传输几次(总线频)
通过性能分析找到瓶颈¶
Julia集合¶
Julia集合是一个CPU密集型问题 可以产生复杂的输出图像的分型数列, 函数包含一个CCPU密集型的组件和一个显式的输入集合
- cProfile
- runsnakerun
- line_profiler
用memory_profiler诊断内存的用量
用heapy调查堆上的对象¶
用dowser实时画出变量的实例¶
用dis模块检查Cpython字节码¶
如何保证单元测试覆盖了所有的代码¶
列表和元组¶
写高性能程序最重要的事情是了解你的数据结构所能提供的性能保证.
高性能编程的很大一部分是了解你查询数据的方式, 并选择一个能迅速响应这个查询的数据结构
列表和元组之类的数据结构被称为数组 列表是动态的数组, 元组是静态的数组
更有效的搜索¶
高效搜索必需的两大要素是 排序算法 和 搜索算法
对次序未知的列表/元组的最优查询时间是O(log n)
bisect模块提供一个简单的函数可以在保持排序的同时往列表中添加元素, 以及一个高度优化过的二分搜索算法函数来查找元素
选择正确的数据结构并坚持使用
列表, 元组¶
- 列表是动态数组, 可变且,而已重新设置长度(改变内部元素个数)
- 元组是静态素组, 不可变, 内部数据一旦创建就无法改变
- 元组缓存于Python运行时环境, 这意味着每次使用元组时无须访问内核去分配内存
列表和元组都可以接受混合类型, 这会带来额外的开销并减少一些可能的优化, 如果强制要求所有的数组是同一个类型, 就可以避免这些开销
可以通过numpy降低内存和计算的开销
对于非数字的数据, 还有其他的模块, blist, array也可以减少这些开销
元组虽然不支持改变大小, 但是我们可以将两个元组合并成一个新元组, 这个操作类似列表的resize操作, 但是任意两个元组合并始终返回一个新分配的元组, 复杂度是O(n), 因为对元组每添加一个新元素都会有分配和复制操作
小结¶
当数据有一个内在次序的时候, 使用,列表和元组可以让速度更快, 开销更低 数据的内在次序使你可以回避在这些数据结构内部查询的问题 如果次序已知, 那么查询操作是O(1), 避免了昂贵的O(n)的线性搜索
字典和集合¶
如果你有一些无序数据,但它们可以被唯一的索引对象来引用(任何可以被散列的类型都可以成为索引对象 索引对象通常会是一个字符串)
字典和集合几乎一模一样, 不过集合不包含值, 集合只是一堆键的组合
可以被散列的类型是一种同时实现了__hash__函数以及__eq__或__cmp__两者之一的类型
字典和集合基于键的查询为O(1), 插入时间为O(1), 但是会占用更多的内存, 同时, 虽然插入/查询复杂度是O(1), 但实际的速度极大取决于其使用的散列函数, 如果散列函数的运行速度较慢, 那么在字典和集合上进行的操作也会相应变慢
散列碰撞
一个不超过三分之二满的表在具有最佳空间节约的同时依然具有不错的散列碰撞避免率
衡量”我的散列函数分不均匀程度”的标准被称为散列函数的熵
迭代器和生成器¶
会消耗大量的内存 divisible_by_three = len([n for n in list_of numbers if n %3 == 0]) 消耗内存远小于列表生成式 divisible_by_three = sum((1 for n in list_of_numbers if n % 3 == 0))
生成器的延迟估值¶
生成器之所以能节约内存是因为它只处理当前感兴趣的值, 在我们计算的任意点, 我们都只能访问当前的值, 而无法访问数列中的其他元素(这种算法通常称为”单通”, “在线”), 这时候生成器会变得难以被使用, 不过有很多模块和函数可以解决这一问题
itertools, 提供了Python内建函数map, reduce, filter, zip的生成器版本(imap, ireduce, ifilter, izip)
islice: 允许对一个无穷生成器进行切片 chain: 将多个生成器链接到一起 takewhile: 给生成器添加一个终止条件 cycle: 通过不断重复将一个有穷生成器变成无穷
小结¶
使用迭代器组织我们的异常检测算法, 我们能处理的数据就远远超过了内存的限制.
矩阵和矢量计算
numpy可以进行高效矢量操作
它能将数据连续存储在内存中并支持数据的矢量操作. 任何我们对numpy数组的数学操作都能自动矢量化而无须我们显式遍历每一个元素, 这样会让矩阵计算更简单, 同时更快
内存问题才是代码效率低下的决定性因素, 瓶颈取决于能否将这些数字以足够快的速度传输给CPU让它能以最高的速度进行计算.
小结 优化: 减少CPU获得数据的时间减少CPU需要干的工作
numpy + memory + laplacian + numexpr
我们应该总是将代码需要的任何管理性工作放在初始化阶段进行, 这可能包括 内存分配, 读取配置文件, 预先计算程序所需要的一些数据等, 原因有两点, 首先在初始化阶段一次性搞定可以让你减少这些工作运行的总次数, 并让你知道你可以在将来不需要付出什么代价就使用这些资源. 其次, 你的程序不会因为要转而去做这些工作而打扰了流程, 这可以让流水线更有效并让缓存始终含有相关数据
编译成C¶
CPython 编译成C的通用工具, 覆盖了numpy和普通的Python代码 Shed Skin 一个用于非numpy代码的, 自动把Python转换成C的转换器 Numba 一个专用于numpy代码的新编译器 Pythran 一个用于numpy和非numpy代码的新编译器 PyPy 一个用于非numpy的代码, 去带常规Python可执行程序的稳定的即时编译器
JIT和AOT¶
提前编译工具(Cpython, Shed Skin, Pythran) 即时编译工具(Numba, PyPy)
为什么类型检查有助代码更快运行¶
Python是动态类型的—-一个变量能够引用任何类型的对象, 并且任意代码行都能够改变被引用对象的类型. 这使得虚拟机难以在机器码层面优化代码的运行方式, 因为它不知道哪种基础数据会用于将来的运算, 让代码保持泛型就会让代码运行更慢.
使用C编译器¶
Cython¶
Cython注解来分析代码块
一般情况下, 可能最消耗CPU时间的代码行是下面这些:
- 在紧凑的内循环内
- 解引用list, array或者np.array这些项
- 执行数学运算 如果不知道哪些代码执行得频繁, 可以使用一个剖析工具, 比如 line_profile
Shed Skin¶
在一台机器上使用OpenMP来做并行解决方案
在禁止GIL时, 我们一定不能再常规Python对象(例如, lists)上操作,必须要在原生对象和支持memoryview接口的对象上去操作. 如果并行操作了常规的Python对象, 我们不得不去解决随之而来的内存管理问题, 而这时GIL意图避免的. Cython不阻止我们去操作Python对象, 但是如果你这样做, 只会招来痛苦和困扰
Numba¶
专用于numpy代码的即时编译器, 在运行时由LLVM编译器来编译
并发¶
并发允许我们在等待一个I/O操作完成的时候执行其他操作
异步编程介绍¶
使用并发, 典型情况下我们会有一个叫做”事件循环”的东西, 来管理我们程序中该运行什么, 什么时候运行. 实质上, 一个事件循环只是需要运行的一个函数列表
使用事件循环编程能采取两种方式: 回调或者future
在回调模式中, 使用一个通常称之为回调的函数作为输入参数来调用函数. 它会使用值来调用回调函数, 而不是把值返回出去
gevent
tornado asyncio
gevent¶
最简单的异步库, 它遵照异步函数返回future的模式, 意味着代码中的大部分逻辑会保持一样. 此外gevent对标准的I/O函数做了猴子补丁. 把它们变成了异步, 这样大多数时间你可以仅仅使用标准的I/O包并得益于异步的行为.
gevent提供了两个机制来使能异步编程, 它用异步的I/O函数给标准库打补丁, 并且它也有一个greenlet对象能被用于并发执行. greenlet是一种协程, 能够被想象成线程, 无论怎样, 所有的greenlets运行在同一物理线程上, 也就是说, gevent的调度器在I/O等待期间使用一个事件循环在所有greenlets间来回切换, 而不是用多个CPU来运行它们. 大多数情况下, gevent通过使用wait函数来设法尽可能透明化地处理事件循环. wait函数将启动一个事件循环, 只要有需要就运行着, 知道所有的greenlets结束. 因此大部分gevent代码以串行方式运行, 接着, 在某点上, 你会设置许多greenlets来做并发任务, 并且用wait函数来启动事件循环. 当wait函数正在执行时, 你入队堆积起来的所有并发任务会运行直到结束(或某个停止条件), 接着你的代码会重新回到串行方式运行.
future由gevent.swap来创建, 使用了一个函数和传递给这个函数的参数, 并且启动了一个负责运行这个函数的greenlet. greenlet能够被看作一个future, 因为你声明的函数一旦运行完成, 它的值就会包含在greenlet的value域中
启动与我们要抓取的URL相同数量的greenlets是没有效率的, 我们需要一种机制来限制我们同时处理的HTTP请求
我们可以通过信号量来手动控制并发请求的数量, 如果需要的话
比如限制同一时刻只从100个greenlets来做HTTP的get请求. 信号量确保了同一时刻只有一定数量的协成能进入上下文模块. 作为结果我们可以启动我们所需的所有greenlets 来立即抓取URLs, 但只有其中100个将会在同一时刻做出HTTP调用.
看代码
179页
grequests
tornado¶
看代码
AsyncIO¶
小结¶
Python3.4+ 中的asyncio允许完全控制一个异步I/O栈. 除了各种各样的抽象级别, 每个库为它的语法使用了一个不同的范型(差异主要源于在Python3 以前缺乏对并发的原生支持以及引入了yield from声明), 可以从这一系列方法中去获取经验, 并给予需要多少底层控制来挑选其中一个
gevent , tornado, asyncio三个库中又轻微的速度差异, 这些速度差异很多都是基于协成的调度方式, 比如tornado做了一件极好的工作来快速启动异步操作并快速让协成继续运行. 另一方面 asyncio看上去运行得稍微糟了一点, 但是它允许访问更底层的API, 并能动态调整
multiprocessing模块¶
CPython默认没有使用多CPU. 一部分原因是Python是被设计用于单核领域, 另一部分原因是实际上有效的并行化是相当困难的.
Amdahl定律
如果你的代码只有一小部分能够并行化, 那就和你给它多少CPU无关.
multiprocessing模块让你使用基于进程和基于线程的并行处理, 在队列上共享任务, 以及在进程间共享数据. 它主要是集中于单机多核的并行(对于多机并行来说, 有更好的选择) 一个很普遍的用法就是针对CPU密集型的问题 在一个进程集上并行化一个任务. 你可能也用它来并行化I/O密集型操作, 但是我们有更好的工具来处理这类问题(asyncio, gevent, tornado)
OpenMP是一个低层的多核接口, multiprocessing在一个更高的层次上工作, 共享Python的数据结构 而OpenMP一旦被编译成C后, 就是用C的原生对象(例如, 整型数和浮点数)来工作, 它只有在你编译你的代码时才有意义去使用, 如果你不去编译(例如, 使用高效的numpy代码并想要在多核上运行), 那么坚持使用multiprocessing可能是正确的途径
在并行系统中共享状态是困难的, 避免共享状态会让你的编码变得简单很多
一个算法能够几乎全凭有多少状态必须要共享来分析出它在并行环境中表现如何, 例如, 如果我们有多个Python进程全部都是解决一个问题, 而彼此之间没有通信, 我们增加越来也多的Python进程也不会招致多大的惩罚
另一方面, 如果每一个进程需要和其他的Python进程通信, 那么通信开销将会慢慢让处理变得不堪重负, 拖慢了事情. 这意味着当我们增加越来越多的Python进程时候, 实际上减慢了整体性能.
multiprocessing模块有一些典型的工作
- 用进程或池对象来并行化一个CPU密集型任务
- 用哑元模块在线程池中并行化一个I/O密集型任务
- 用队列来共享捎带的工作
- 在并行工作者之间共享状态, 包括字节, 原生数据类型, 字典和列表
Python中的线程是OS原生的(他们不是模拟出来的, 他们是真实的操作系统线程) 它们被全局解释锁(GIL)所束缚, 我们同一个时刻只有一个线程可以和Python对象交互
使用进程, 我们可以并行运行一定数量的Python解释器, 每一个进程都有私有的内存空间, 有自己的GIL锁, 每一个都串行运行(没有GIL之间的竞争)
multiprocessing模块综述¶
主要组件
进程
一个当前进程的派生(forked)拷贝,创建了一个新的进程标识符, 并且任务在操作系统中以一个独立的紫禁城运行。 你可以启动并查询进程的状态并给它提供一个目标方法来运行。
池
包装了进程或线程。 在一个方便的工作者线程池中共享一块工作并返回聚合的结果
队列
一个先进先出(FIFP)FIFP的队列允许多个生产者和消费者。
管理者
一个单向或双向的唉两个进程间的通信渠道
ctypes
允许在进程派生(forked)后,在父子进间共享原生数据类型(例如,整型数,浮点数,字节数)
同步原语
锁和信号量在进程间同步控制流
windows缺少fork, 所以multiprocessing模块试驾了一下windows特有的约束, 所以要使用windows平台, 需要注意
使用进程间通信来验证素数(素数下的方案需要花时间看一下)¶
素数是除了自己和1以外没有其他因子的数字.
串行解决方案¶
Naive Pool 解决方案¶
Naive Pool解决方案使用一个 multiprocessing.Pool 来工作
Less Naive Pool 解决方案¶
使用Manager.Value 作为一个标记¶
使用redis作为一个标记¶
- 使用 RawValue 作为一个标记
- 使用 mmap 作为一个标记
用 multiprocessing 来共享 numpy 数据¶
当工作于大numpy数组时候, 可以在进程间为读写存取来共享数据, 而不用拷贝数据
在进程间共享一个大矩阵有几个收益
- 只有一个拷贝意味着没有浪费RAM
- 不浪费时间来拷贝大块内存
- 你得到了在进程间共享部分结果的可能性
使用numpy估算pi的时候, 随机数生成是一个串行的过程
我们可以派生(fork)进程共享一个大数组, 每一个进程使用不同种子的随机数生成器, 用随机数来填充数组的一个区域, 因此生成完一个大随机块可能要比用一个单独的进程要快
同步文件和变量访问¶
文件锁¶
给 value 加锁¶
multiprocessing小结¶
一般情况, 推荐使用一个外部的队列库, 这样队列的状态更加透明. 我们应该倾向于使用一个容易阅读的工作格式而不是序列化(pickled)的数据, 这样容易调试
仅仅使用一个天真的并行方式(没有IPC)可能是有意义的.购买一台具有更多核的更快的计算机可能比设法使用IPC来开发一台现有的机器要现实得多.
不做拷贝的并行共享numpy矩阵仅仅对于一小撮问题是重要de,但是当它重要时, 它就真的重要. 确保你真的没有在进程间拷贝数据需要花费额外的几行代码和一些安全检查
集群和工作队列¶
一个集群通常被视作一组共同工作来解决公共问题的计算机集合. 从外部看, 它可能就是一个更大的独立系统
集群的益处¶
最明显的益处就是能够轻易地扩展计算机需求—-如果需要处理更多的数据或者得到更快的答案, 只需要增加更多的机器
通过增加机器, 也可以提高可靠性, 一定数量的节点失效不会导致整个集群停止工作
集群也可以用来创建动态扩展的系统, 比如淘宝双十一
机器激活时间足够快从而赶上处理需求变化的速度, 动态扩展就是处理非均匀的应用模式的一种非常节约成本的方式
集群的缺陷¶
你需要考虑当你增加机器时会发生什么, 比如, 机器间的延迟, 你需要知道节点是否在工作. 系统管理也会是一个挑战
设计集群的时候, 你要考虑机器间配置不同的可能性, 不同机器可能有不同负载, 不同的局部数据.该如何处理数据转移? 移动任务跟数据涉及的延迟会成为问题吗?
你的任务需要和其他任务相互通信吗? 当几个任务正在运行时, 如果一个进程失效了, 或者一台机器挂了或者一些硬件出现问题, 会发生什么? 你必须考虑这些问题.
同时也要考虑到失效使能被接收的, 当你运行一个基于内容的web服务, 你可能不需要99.999%的可靠性, 接受一点失效常常可以降低边际工程和管理成本. 不过, 如果一个高频交易系统经历了失效, 那么造成的代价会是相当大的.
同时需要对一些可能出现的问题定制文档, 比如如果没有一个文档化的重启计划, 那你就可能在最坏的时候来写文档.
考虑到网络的复杂性以及发生失效的升级, 这个失效可能是难以预测和做出计划来应对的, 网络上的所有节点不会失效的理由是因为不同的软件版本和不同的平台–异构网络比同构网络更具有可靠性的收益
怎样启动一个集群化的解决方案¶
从一台服务器开始…
可以通过不可预料的方式使代码失效(比如 kill -9 <pid>,
拔掉机器的电源), 来检查系统的健壮性
同时可以做更重量级的测试 – 充满编码错误, 和人工异常的单元测试集
一旦有了一个可靠的集群, 你就可以引入类似Netflix的ChaosMonkey那样的随机杀手工具来故意杀掉你的部分系统来测试它们的弹性.
使用集群时避免痛苦的方法¶
使用更少的RAM¶
基本类型的对象开销高¶
一个具有 1 00 000 000 项的list大概要消耗760MB, 这是假设所有条目都是相同对象的前提下.
Array模块以廉价的方式存储了许多基础对象
Array模块高效地存储了类似与证书, 浮点数和字符的基础类型, 但没有复数或者类. 它创建了一个连续的RAM块来保存底层数据.
array模块使用一个有限的具有各种不同精度的datatype集来工作. 选择你需要的最小精度, 这样你就会按需分配RAM, 而不是分配超出需求更多的RAM. 要注意字节的大小是平台相关的 – 这里的大小参考32平台, 而我们却是在一台64位的笔记本上运行.
numpy具有能够持有广泛的datatypes的数组 – 你对每一项的字节的数量有更多的控制, 并且你还可以使用复数和datetime对象, 一个complex128对象采用每项16个字节: 每项是一个8字节的浮点数对. 你不能在一个Python数组中存储复杂的对象, 但是它们在numpy中是自由使用的.
无论如何, 如果你在python的array内容上做任何工作, 基础类型可能被转换成临时对象, 抵消了它们的收益. 当和其他进程通信时把它们当成数据存储来使用是array的一个很棒的使用场景.
如果你在做重量级的数学运算, numpy数组肯定是一个更好的选择, 因为你得到了更多的datetype选项和许多专业而快速的函数. 如果你想要让你的项目有更少的依赖性, 你可能选择避开numpy, 尽管Cython和Pythran用array和numpy数组同样都工作得好, Numba只用numpy数组来工作.
理解集合中的RAM使用¶
python的 sys.getsizeof(obj)
调用会告诉我们一些关于对象所使用的内存情况(绝大多数而不是全部对象提供了这个调用)
getsizeof只报告了一部分开销, 常常仅仅是父对象的开销. 它不总是被实现了的, 所以可以作优先的用途
一个轻量级的更好的工具是 asizeof ,
它会遍历容器的层级结构并对它所发现的每个对象做出最好的猜测,
给整体大小增加了尺寸, 但是它的速度相当慢.
除了依赖于猜测和假设之外, 它也不能计算幕后的内存分配(例如, 一个包装了C库的模块可能没有报告在C库中所分配的字节数). 最好把它用来作为一个知道. 我们倾向于memit, 因为它给了我们在问题机器上的准确的内存使用计数.
字节的Unicode的对比¶
高效地在RAM中存储许多文本¶
使用文本的一个普遍的问题就是它占用了许多RAM – 但是如果我们想要测试一下我们是否在之前见到过字符串或对它们的频率进行技术, 那么让它们在RAM中是方便的, 而不是让它们从磁盘中来回幻夜. 以自然的方式存储字符串是代价昂贵的, 但是trie数和有向无环的单词图(DAWGs)能被用来压缩表示它们, 同时又允许进行快速的操作
这些更高级的算法能够让你显著节约RAM的使用量, 这意味着你可能不需要扩展到更多的服务器上, 对于生产系统有巨大的节省.
使用 trie树 压缩一个字符集串, 可以大量降低内存消耗,
同时性能仅仅只是发生微小的变化.
看代码
DAFSA是DAWG的另一个名字
DAWG和trie树的主要区别是 trie树只共享公共前缀, 而DAWG共享公共前缀和后缀. 在有很多公共单词前缀和后缀的语言中(就像英语), 这样能减少很多的重复.
精确的内存表现取决于你的数据结构, 典型来说, 一个DAWG不能分配一个值给键, 这归因于从字符串的开始到结束之间的多条路径, 但是展示在这里的版本能够接收一个值映射. Trie树也能接收一个值映射
有向无环单词图(DAWG)
有向无环图(MIT授权)企图高效地表示共享公共前缀和后缀的字符串
Marisa trie树
Marisa trie树(LGPL和BSD双授权)是一个使用Cython绑定外部库的静态trie树. 因为是静态的, 所以在创建之后就不能改动. 它像DAWG一样支持把整数索引作为值与字符值记录值一起存储
一个键能被用来查询一个值, 反之亦然. 所有共享了相同前缀的键能被高效地找到. tries树的内容可以被持久化
Datrie树
双数组trie树, 或者datrie(LGPL授权), 使用了预先构建的字母表来高效地存储键. 这种trie树能够在创建之后修改, 但是只能使用相同的字表. 它也能够寻找与所提供的键共享前缀的所有键, 并且支持持久化.
HAT trie树
HAT trie树(MIT授权)使用了缓存友好的表达式从而在现代CPU上达成非常快速的查找. 它能够在创建后被修改, 但是另一方面只有非常有限的API
对于简单的使用场景, 它具有很棒的性能, 但是API的局限性(例如, 缺少前缀查找)可能对你的应用来说用途更少.
在生产系统中使用trie树和DAWGs
trie树和DAWG数据结构提供了良好的收益, 但它们在生产系统却提供了强大的收益.
使用trie树的一个案例
使用更少RAM的窍门¶
如果你正在使用数字型的数据来工作, 那么你肯定会想要转而使用numpy数组, 该包提供了许多直接工作在底层基本类型对象上的快速算法, 与使用列表相比, RAM上的节省是巨大的, 同时时间上的节省也令人惊奇
如果你正在使用字符串工作, 并且你用的是python2.x, 设法使用str而不是unicode就可以节省RAM, 如果你贯穿整个程序需要许多Unicode对象, 你可能通过简单的升级到python3.3+, 就能享受更好的服务. 如果你正要在一个静态结构中存储大量的Unicode对象, 那么你可能想要调查下DAWG和trie数结构
如果你征用许多比特字串来工作, 调查一下numpy和bitarry包, 他们都有把比特打包进字节的高效表示, 你可能也会受益于查看redis, 他提供了高效的比特模式存储
PyPy项目正在试验更高效的同质数据结构的表示, 这样相同的基本类型的长列表在PyPy中要比在CPython中的等价结构体可能消耗要少得多.
概率数据结构¶
概率数据结构允许你以精度来换取大幅度的内存使用下降. 除此之外, 你能在它们之上所做的操作数量比set活着trie树要有限得多.