本文基于Docker官方网站的Mac OSX教程所写。

安装Docker Toolbox

Docker Toolbox包含如下的Docker工具:

  • Docker CLI 用于创建Docker镜像和Docker容器
  • Docker Machine 让你可以在Mac OSX以命令行的方式操作Docker引擎
  • Docker Compose用于运行docker-compose命令
  • Kitematic —— Docker的GUI组件
  • Docker快速启动shell
  • Oracle VM 虚拟机

步骤一:检查系统版本号

检查操作系统版本号,版本号必须 >= 10.8

步骤二:安装Docker Tool

下载Docker Toolbox并开始进行安装,安装很傻瓜,一路next就可以。

步骤三:检查是否安装成功

  1. 首先开启Docker Quickstart Terminal

  2. 终端会有如下输出

    Last login: Sat Jul 11 20:09:45 on ttys002
    bash '/Applications/Docker Quickstart Terminal.app/Contents/Resources/Scripts/    start.sh'
    Get http:///var/run/docker.sock/v1.19/images/json?all=1&filters=%7B%22dangling%22%3A%5B%22true%22%5D%7D: dial unix /var/run/docker.sock: no such file or directory. Are you trying to connect to a TLS-enabled daemon without TLS?
    Get http:///var/run/docker.sock/v1.19/images/json?all=1: dial unix /var/run/docker.sock: no such file or directory. Are you trying to connect to a TLS-enabled daemon without TLS?
    -bash: lolcat: command not found
    
    mary at meepers in ~
    $ bash '/Applications/Docker Quickstart Terminal.app/Contents/Resources/Scripts/start.sh'
    Creating Machine dev...
    Creating VirtualBox VM...
    Creating SSH key...
    Starting VirtualBox VM...
    Starting VM...
    To see how to connect Docker to this machine, run: docker-machine env dev
    Starting machine dev...
    Setting environment variables for machine dev...
    
                        ##         .
                  ## ## ##        ==
               ## ## ## ## ##    ===
           /"""""""""""""""""\___/ ===
      ~~~ {~~ ~~~~ ~~~ ~~~~ ~~~ ~ /  ===- ~~~
           \______ o           __/
             \    \         __/
              \____\_______/
    
    The Docker Quick Start Terminal is configured to use Docker with the “default” VM.
    
  3. 键入docker run hello-world

    $ docker run hello-world
    Unable to find image 'hello-world:latest' locally
    latest: Pulling from library/hello-world
    535020c3e8ad: Pull complete
    af340544ed62: Pull complete
    Digest: sha256:a68868bfe696c00866942e8f5ca39e3e31b79c1e50feaee4ce5e28df2f051d5c
    Status: Downloaded newer image for hello-world:latest
    
    Hello from Docker.
    This message shows that your installation appears to be working correctly.
    
    To generate this message, Docker took the following steps:
    1. The Docker Engine CLI client contacted the Docker Engine daemon.
    2. The Docker Engine daemon pulled the "hello-world" image from the Docker Hub.
    3. The Docker Engine daemon created a new container from that image which runs the executable that produces the output you are currently reading.
    4. The Docker Engine daemon streamed that output to the Docker Engine CLI client, which sent it to your terminal.
    
    To try something more ambitious, you can run an Ubuntu container with:
    $ docker run -it ubuntu bash
    
    Share images, automate workflows, and more with a free Docker Hub account:
    https://hub.docker.com
    
    For more examples and ideas, visit:
    https://docs.docker.com/userguide/
    

这里对网络IO做一次总结

《Unix网络编程》一书中对网络IO模型的总结如下:

  • 同步模型(synchronous I/O)
    • 阻塞I/O(bloking I/O)
    • 非阻塞I/O(non-blocking I/O)
    • 多路复用I/O(multiplexing I/O)
    • 信号驱动式I/O(signal-driven I/O)
  • 异步I/O(asynchronous I/O)

阻塞I/O

阻塞IO是最常用的模式,进程发起recvfrom系统调用后,进程就被阻塞了,然后等待数据准备好,数据从内核态拷贝到用户态后,进程再进行处理。

非阻塞I/O

非阻塞IO正好相反,进程发起recvfrom系统调用后不会阻塞,马上会得到返回结果,如果此时没有数据,会返回EWOULDBLOCK,用户进程可以不断地尝试recefrom,然后有数据处理的时候,函数调用仍然是阻塞的。

多路复用I/O

多路复用有两个系统调用selectpoll,就select函数来说,其可以监听多个文件描述符,当有数据可以处理的时候,系统调用将会返回,然后用户进行可以调用recvfrom进行数据读取。

异步I/O

用户进程进行aio_read系统调用之后,无论内核数据是否准备好,都会直接返回给用户进程,然后用户态进程可以去做别的事情。等到socket数据准备好了,内核直接复制数据给进程,然后从内核向进程发送通知。

同步和异步模型的区别

关于同步和异步,我的理解是数据的获取方式:同步模型,数据是用户态尝试去向内核态获取数据的;异步模型,数据是从内核态拷贝到用户态,并通知用户进程数据ok了。

关于epoll

epoll到底是同步还是异步的呢?
我们知道epoll属于多路复用IO,那么它应该是同步的。但是,epoll因为采用mmap的机制,使得内核socket buffer和用户空间的buffer共享,从而省去了数据拷贝,这也意味着,当epoll回调上层的 callback函数来处理socket数据时,数据已经从内核层“自动“”到了用户空间。
stevens老爷子写《Unix网络编程》已经是上个世纪的事情了,当时epoll还没有,我觉得epoll不属于任何一种吧。

本文是基于《C++并发编程》第三章的读书笔记

1.条件竞争(race condition)

并发中竞争条件的形成,取决于一个以上线程的相对执行顺序,每个线程都抢着完成自己的任务。大多数情况下,即使改变执行顺序,也是良性竞争,其结果可以接受。恶性条件竞争通常发生于完成对多于一个的数据块的修改时,例如,对两个连接指针的修改。因为操作要访问两个独立的数据块,独立的指令将会对数据块将进行修改,并且其中一个线程可能正在进行时,另一个线程就对数据块进行了访问。
保护共享数据结构的最基本的方式,是使用C++标准库提供的互斥量(mutex)。

2.C++11的互斥器

上一篇实现了一个互斥器,看一下C++11提供的互斥器的用法。

#include <mutex>

std::mutex one_mutex;

{
    std::lock_guard<std::mutex> guard(one_mutex);
    ... ...
}

3. 死锁:解决方案

避免嵌套锁

第一个建议往往是最简单的:一个线程已获得一个锁时,再别去获取第二个(don’t acquire a lock if you already hold one)。当你需要获取多个锁,使用一个std::lock来做这件事(对获取锁的操作上锁),避免产生死锁。

避免在持有锁时调用用户提供的代码

第二个建议是次简单的:因为代码是用户提供的,你没有办法确定用户要做什么;用户程序可能做任何事情,包括获取锁。

使用固定顺序获取锁

当硬性条件要求你获取两个以上(包括两个)的锁,并且不能使用std::lock单独操作来获取它们;那么最好在每个线程上,用固定的顺序获取它们获取它们(锁)。

好长一段时间没有写文章了,这次总结一下陈硕大大写的《Linux多线程服务端编程》一书第二章的读书笔记。

1.互斥器(mutex)

使用互斥器的基本原则:

  • 用RAII手法封装mutex的创建、销毁、加锁、解锁这四个操作。Java里面的synchronized语句和C#的using语句也有类似的效果,即保证所得生效期间等于一个作用于,不会因异常而忘记解锁。
  • 只用非递归的mutex(即不可重入的mutext)
  • 不手工调用lock()和unlock()函数,一切交给栈上的Guard对象的构造和析构函数负责。
  • 在每次构造Guard对象的时候,思考一路上(调用栈上)已经持有的锁,防止因为锁顺序不同而导致死锁。
  • 进程间通信使用socket,不要使用mutex。
  • 加锁、解锁在同一个线程(RAII会保证)。
  • 别忘记解锁(RAII会保证)。
  • 不要重复解锁(RAII会保证)。
  • 必要的时候可以考虑用PTHREAD_MUTEX_ERRORCHECK来排错。

基本的互斥器的代码如下:

class MutexLock {
public:
    MutexLock() {
        pthread_mutex_init(&_mutex, NULL);
    }

    ~MutexLock() {
        pthread_mutex_destroy(&_mutex);
    }

    MutexLock& operator=(MutexLock& other) = delete;
    MutexLock(MutexLock& other) = delete;

    void lock() {
        pthread_mutex_lock(&_mutex);
    }

    void unlock() {
        pthread_mutex_unlock(&_mutex);
    }

    pthread_mutex_t* getRawMutex() {
        return &_mutex;
    }

private:
    pthread_mutex_t _mutex;
};

class MutexGuard {
public:
    MutexGuard(MutexLock& mutex)
    :_lock(mutex) {
        _lock.lock();
    }

    ~MutexGuard() {
        _lock.unlock();
    }
private:
    MutexLock& _lock;
};

2.条件变量(condition variable)

如果需要等待某个条件成立,我们应该使用条件变量。条件变量顾名思义是一个或多个线程等待某个布尔表达式为真,即等待别的线程“唤醒”它。条件变量的学名叫管程(monitor)。Java Object内置的wait()、notify()、notifyAll()是条件变量。
对于wait端应该注意:

  • 必须与mutex一起使用,该布尔表达式的读写接收此mutex的保护
  • 在mutex已经上锁的时候才能调用wait()
  • 把判断布尔条件和wait()放到while循环中

如下是一个简单的Condition实现

class Condition {
public:
    Condition(MutexLock& mutex)
    :_mutex(mutex) {
        pthread_cond_init(&_cond, NULL);
    }

    ~Condition() {
        pthread_cond_destroy(&_cond);
    }

    void wait() {
        pthread_cond_wait(&_cond, _mutex.getRawMutex());
    }

    void notify() {
        pthread_cond_signal(&_cond);
    }

    void notifyAll() {
        pthread_cond_broadcast(&_cond);
    }
private:
    MutexLock& _mutex;
    pthread_cond_t _cond;
};

关于Spurious wakeup(虚假唤醒)

虚假唤醒在linux的多处理器系统中,在程序接收到信号前可能会发生。在Windows系统和JAVA虚拟机上也存在。在系统设计时应该可以避免虚假唤醒,但是这会影响条件变量的执行效率,而既然通过while循环就能避免虚假唤醒造成的错误,因此程序的逻辑就变成了while循环的情况。

3.copy-on-write

主要思想是这样的:让读的加锁粒度尽量的小,但当写数据的时候,新的读请求是阻塞的。

typedef std::shared_ptr<Map> MapPtr;    
MapPtr gData(new Map);     // 有一个数据是需要共享的

MapPtr getData() {   // 读数据只有这一段是加锁的
    MutexGuard guard(mutex);
    return gData;
}

// read threads
MapPtr _data = getData(); 
// process _data....

// write thread
MutexGuard guard(mutex);    // 写数据全场加锁
if(!gData.unique()) {
    MapPtr tmpData(new Map(gData.get()));
    gData.swap(tmpData);
}
// process _data ...

4.小结

记得两年前开始了解C++多线程编程的时候看了陈硕的这本书,工作两年,由于长时间使用Python,对C++已经有些陌生了,更别说C++多线程编程了,今天再次拾起这本书,重读前面两章就当复习了。
接下来我会抽周末看一下《C++ Concurrency In Action》,也会陆续写一些相关的读书笔记。

周末在家花了半天时间写了一个简单的双向循环链表,为什么要这样做,一是想比较一下Python原生的list和双向链表的性能差距是怎样的;二是看libuv的QUEUE源码,感觉libuv实现双向循环链表的方式还是很优雅的,打算借用一下。

代码地址:https://github.com/Whosemario/ThinkIdeaEx/tree/master/python_list_ex

安装

> python setup.py build_ext --inplace

本地会生成一个listex.so的动态链接库。

性能对比

1.append方法

双向循环链表测试代码:

import time
import listex

li = listex.listex()

start_ts = time.time()
for i in xrange(0, 10000000):
    li.append(i)
end_ts = time.time()

print "total time:", end_ts - start_ts

输出结果如下:

total time: 2.71579694748

原生Python List测试代码:

import time
import listex

li = listex.listex()

start_ts = time.time()
for i in xrange(0, 10000000):
    li.append(i)
end_ts = time.time()

print "total time:", end_ts - start_ts

输出结果如下:

total time: 1.8892519474

上面的结果可以看出来Python List的append的效率更高,这个是在意料之中的,Python List向后插入本来性能就不差,原因是Python List在容量不足时候会扩容,容量足够的时候append的内部实现就是数组赋值,性能肯定不差;而我的listex,在每次插入一个PyObject的时候,都要malloc一个队列节点(ListexNode), 这个性能就会差很多了。

2.pop方法

原生Python List从下标为0的地方pop出值:

import time
import listex

li = []
for i in xrange(0, 100000):
    li.append(i)

start_ts = time.time()
for i in xrange(0, 100000):
    li.pop(0)
end_ts = time.time()

print "total time:", end_ts - start_ts

输出结果如下:

total time: 2.07851791382

使用listex,获取第一个listex node,并从队列中剔除:

import time
import listex

li = listex.listex()
for i in xrange(0, 100000):
    li.append(i)

start_ts = time.time()
for i in xrange(0, 100000):
    li.get_head().remove()
end_ts = time.time()

print "total time:", end_ts - start_ts

输出几个如下:

total time: 0.0255160331726

这种情况双向链表的优势就很明显了,Python List每次都要从头部pop出一个值,因此每次都要进行数据的整体移动,性能就很耗了。

关于此工程

现在这个双向链表的代码还是不完整,后面将会持续更新。

Redis的安装还是很简单的,但作为学习Redis的第一篇文章,还是简单介绍一下Redis的安装

1.下载Redis

下载地址:http://download.redis.io/releases/redis-2.8.24.tar.gz

2.安装Redis

简单安装

% make

make就可以简单安装了。

内存分配器

想选择一个非默认的内存分配器,可以定义MALLOC环境变量
使用libc malloc:

% make MALLOC=libc

使用jemalloc:

% make MALLOC=jemalloc

更多的输出信息

如果希望看到更多的输出信息,使用下面方式安装

% make V=1

3.使用Redis

开启服务端

% cd src/
% ./redis-server

我们可以通过一个配置文件启动redis服务器

% cd src/
% ./redis-server /path/to/redis.conf

也可以直接输入参数

% ./redis-server --port 9999 --slaveof 127.0.0.1 6379
% ./redis-server /etc/redis/6379.conf --loglevel debug

开启客户端

% cd src
% ./redis-cli
redis> ping
PONG
redis> set foo bar
OK
redis> get foo
"bar"
redis> incr mycounter
(integer) 1
redis> incr mycounter
(integer) 2
redis>

Redis命令说明:
http://redis.io/commands

本文讲一下kqueue的基本使用以及在libuv中uv__io_poll函数的kqueue部分的实现

1.kqueue简介

int kqueue(void)

生成一个内核事件队列,并返回队列的描述符。

struct kevent

struct kevent { 
    uintptr_t ident;        /* 事件 ID */ 
    short     filter;       /* 事件过滤器 */ 
    u_short   flags;        /* 行为标识 */ 
    u_int     fflags;       /* 过滤器标识值 */ 
    intptr_t  data;         /* 过滤器数据 */ 
    void      *udata;       /* 应用透传数据 */ 
 }; 
ident

一般设置为文件描述符

filter

内核检查ident上注册的filter的状态,状态发生变化就通知应用程序,Tcp相关的filter如下:

  • EVFILT_READ:Socket描述符处于监听状态,当有链接进来的时候,data表示就绪队列的链接的数量;对于其他的描述符,当数据可接受的时候返回,并且data是数据可读的数量
  • EVFILT_WRITE:当写入缓冲区可写的时候返回,data表示写入缓冲区的大小。
flags
  • EV_ADD:在kqueue中增加一个event
  • EV_DELETE:在kqueue中删除一个event
fflags

filter相关的flag

data

filter相关的数据

udata

用户自定义的数据

EV_SET 宏定义

EV_SET(&kev, ident, filter, flags, fflags, data, udata);

EV_SET用于初始化struct kevent

kevent方法

int
kevent(int kq, const struct kevent *changelist, int nchanges, struct kevent *eventlist, int nevents, const struct timespec *timeout);

kevent函数用于注册和删除事件,并返回就绪事件或者错误事件。

  • kq:kqueue方法返回的描述符
  • changelist:注册或者删除的事件列表
  • nchanges:changelist数量
  • eventlist:满足条件的事件通知数组
  • nevents:eventlist数量
  • timeout:超时时间,timeout是nullptr,kevent无限期等待,否则超时返回

2.libuv怎样使用kqueue

loop初始化

# in loop.c
int uv_loop_init(uv_loop_t* loop) {
    ...
    err = uv__platform_loop_init(loop);
    ...
}

# in darwin.c 
int uv__platform_loop_init(uv_loop_t* loop) {
      loop->cf_state = NULL;

      if (uv__kqueue_init(loop))
        return -errno;

      return 0;
}

# in kqueue.c
int uv__kqueue_init(uv_loop_t* loop) {
      loop->backend_fd = kqueue();
      ...
  }

本文在MacOSX上进行调试程序,所以平台相关的代码是基于darwin的。看到在loop进行初始化的时候,会对backend_fd参数赋值为kqueue()。

struct kevent 初始化

# in kqueue.c

while (!QUEUE_EMPTY(&loop->watcher_queue)) {
    // 【1】从watcher_queue中取出uv__io_t
    q = QUEUE_HEAD(&loop->watcher_queue);
    ...
    w = QUEUE_DATA(q, uv__io_t, watcher_queue);
    ...
    if ((w->events & UV__POLLIN) == 0 && (w->pevents & UV__POLLIN) != 0) {
      filter = EVFILT_READ;
      fflags = 0;
      op = EV_ADD;
      // 【2】初始化kevent,filter=EVFILT_READ,flags=EV_ADD
      EV_SET(events + nevents, w->fd, filter, op, fflags, 0, 0);

      if (++nevents == ARRAY_SIZE(events)) {
        // 【3】通过kevent函数注册时间
        if (kevent(loop->backend_fd, events, nevents, NULL, 0, NULL))
          abort();
        nevents = 0;
      }
    }

    if ((w->events & UV__POLLOUT) == 0 && (w->pevents & UV__POLLOUT) != 0) {
      // 【4】初始化kevent,filter=EVFILT_WRITE
      EV_SET(events + nevents, w->fd, EVFILT_WRITE, EV_ADD, 0, 0, 0);

      if (++nevents == ARRAY_SIZE(events)) {
        if (kevent(loop->backend_fd, events, nevents, NULL, 0, NULL))
          abort();
        nevents = 0;
      }
    }

    w->events = w->pevents;
}

libuv中的对struct kevent还是蛮清晰的,对于POLLIN类型,加入EVFILT_READ类型的kevent(见【2】),对于POLLOUT类型,加入EVFILT_WRITE类型的kevent(见【4】)。

数据监听

# in kqueue.c

nfds = kevent(loop->backend_fd,
              events,
              nevents,
              events,
              ARRAY_SIZE(events),
              timeout == -1 ? NULL : &spec);
...
for (i = 0; i < nfds; i++) {
  ...
  w = loop->watchers[fd];
  ...
  //【1】 处理读数据
  if (ev->filter == EVFILT_READ) {
    if (w->pevents & UV__POLLIN) {
      revents |= UV__POLLIN;
      w->rcount = ev->data;
    } else {
      ...
    }
  }
  //【2】 处理写状态
  if (ev->filter == EVFILT_WRITE) {
    if (w->pevents & UV__POLLOUT) {
      revents |= UV__POLLOUT;
      w->wcount = ev->data;
    } else {
      ...
    }
  }
  //【3】处理错误
  if (ev->flags & EV_ERROR)
    revents |= UV__POLLERR;

  if (revents == 0)
    continue;

  //【4】回调函数
  w->cb(loop, w, revents);
  ...
}

这里处理满足条件的事件,分别包括读数据、写数据、错误状态。最后回调callback。

3.参考资料

  1. 使用 kqueue 在 FreeBSD 上开发高性能应用服务器 - http://www.ibm.com/developerworks/cn/aix/library/1105_huangrg_kqueue/index.html

粗略地讲一下个人对Python服务器Hotfix的理解

1.Hotfix期望目标

  1. Python服务器不停机
  2. 不需要重新赋值实例的__class__
  3. 不需要重新赋值module

基于上面的期望目标,需要怎样实现Hotfix?

2.Hotfix基本思路

怎样保持Module的引用不变

方案1

最容易想到的就是reload方法,其实reload方法内部实现也是分别调用find_moduleload_module两个方法,在load_module方法中如果sys.module中存在需要加载的Module,那么加载module的内容是在老module的基础上加载的。这样我们会发现reload前后module的id是相同的。

方案2

单独调用imp.find_module方法和imp.load_module两个方法,当然前提也是要加载的Module在sys.module中存在。

Class不替换而是修改内部的function

方案1

此方案是基于Module不变的方案1来做的,在reload之前,保存老的module的__dict__内容,然后reload之后,去尝试更新老的__dict__中的方法,并将其再次赋值给新的module。

方案2

此方案也对应Module不变的方案2来做的,我们需要利用sys.meta_path来hook住Python的find_module函数(原因我在这里讲过),从而在import的过程中尝试替换新的module的Class和Function。

3.小结

上面说的都很抽象,原因是上面的有些思路工作在用,不愿意具体讲,其次这里只是做一些个人的理解,等着合适的时候写一个Python服务器Hotfix的功能模块。

这篇文章主要是写一些自己对Python原生Dict的理解和感悟

1.对比

游戏里面很多策划表是以Python Dict的形式存在的,并且这些Python Dict是常驻内存的,在Python的GC机制里面也是存在于最老的generation里面的。那么Python Dict到底有多耗内存呢?我利用pymalloctrace做过一个简单的对比。

Case One:

构建一个如下数据

d = {
    1 : {1:1, 2:4, 3:9, ... 19:361}
    2 : {...}
    ...
    9999: {...}
}

利用pytracemalloc看一下它的内存占用量到底有多少,测试代码如下:

 1 # -*- coding:utf-8 -*-
 2
 3 import os
 4 import sys
 5 import tracemalloc
 6
 7 tracemalloc.start()
 8
 9 d = {}
10 for i in xrange(1, 10000):
11     sub_d = {}
12     for j in xrange(1, 20):
13         sub_d[j] = j*j
14     d[i] = sub_d
15
16 snapshot = tracemalloc.take_snapshot()
17 statis = snapshot.statistics("lineno")
18 total = 0
19 for stat in statis:
20     total += stat.size
21
22 for stat in statis[:10]:
23     print stat
24
25 print total / 1024.0 / 1024.0, "M"

最后输出的结果:

test_dict.py:13: size=8208 KiB, count=10731, average=783 B
test_dict.py:11: size=2734 KiB, count=9999, average=280 B
test_dict.py:14: size=768 KiB, count=1, average=768 KiB
test_dict.py:10: size=229 KiB, count=237, average=992 B
test_dict.py:16: size=456 B, count=1, average=456 B
11.66065979 M

11M,占用最多的地方就是在Dict被赋值的地方。

Case Two

接下来换一种方式:数据使用tuple进行存储,索引用dict,输入格式如下:

value_index = {1 : 0, 2 : 1, ... 19: 18}
top_index = {1 : 0, 2 : 1, ... 9999 : 9998}
d = (
    (1, 4, 9, ..., 361),
    (1, 4, 9, ..., 361), 
    ...
)

测试代码如下:

   1 # -*- coding:utf-8 -*-
 2
 3 import os
 4 import sys
 5 import tracemalloc
 6
 7 def sub_gen():
 8     for i in xrange(1, 20):
 9         yield i*i
10
11 def gen():
12     for i in xrange(1, 10000):
13         yield tuple(sub_gen())
14
15 tracemalloc.start()
16
17 value_index = {}
18 for i in xrange(1, 20):
19     value_index[i] = i - 1
20
21 top_index = {}
22 for i in xrange(1, 10000):
23     top_index[i] = i - 1
24
25 d = tuple(gen())
26
27 snapshot = tracemalloc.take_snapshot()
28 statis = snapshot.statistics("lineno")
29 total = 0
30 for stat in statis:
31     total += stat.size
32
33 for stat in statis[:10]:
34     print stat
35
36 print total / 1024.0 / 1024.0, "M"

运行结果如下:

test_tuple.py:13: size=2031 KiB, count=10000, average=208 B
test_tuple.py:23: size=998 KiB, count=239, average=4278 B
test_tuple.py:9: size=478 KiB, count=494, average=992 B
test_tuple.py:22: size=229 KiB, count=237, average=992 B
test_tuple.py:12: size=229 KiB, count=237, average=992 B
test_tuple.py:25: size=78.0 KiB, count=2, average=39.0 KiB
test_tuple.py:19: size=768 B, count=1, average=768 B
test_tuple.py:27: size=456 B, count=1, average=456 B
test_tuple.py:21: size=280 B, count=1, average=280 B
3.95300292969 M

结果才3M,为什么会差距这么大,原因就是在tuple和dict的内部实现的不同。

2.Dict内部实现说明

Python Dict是一个Hash表,其解决冲突的方式是开放寻址法。在Dict中存储数据是一个线性数组,key值通过hash函数转换为一个整数,对应于数组中的索引,如果此索引下的数据没有被占用,怎将数据存储;否则,当存在冲突的时候,开放寻址法利用冲突函数找到下一个索引,重复上面的步骤。
基于某些理论,当数组中可用空间小于1/3时,此线性数据将会进行扩展,下面是Python Dict中在插入数据之后会进行检查,如果空间不满足要求则进行扩展。

# in dictobject.c
static int
dict_set_item_by_hash_or_entry(register PyObject *op, PyObject *key,
                           long hash, PyDictEntry *ep, PyObject *value)
{
    ....
    if (!(mp->ma_used > n_used && mp->ma_fill*3 >= (mp->ma_mask+1)*2))
        return 0;
    return dictresize(mp, (mp->ma_used > 50000 ? 2 : 4) * mp->ma_used);
}

我们看到最后一段,dict的扩容将会以使用数量的2倍或者4倍进行扩容,保守上说整个线性数组中有1/3的空间是没有用的。

3.Tuple内部实现说明

tuple的内部实现则很简单。

typedef struct {
    PyObject_VAR_HEAD
    PyObject *ob_item[1];
} PyTupleObject;

ob_item存储tuple数据,可以理解为一个线性的数组,其大小在初始化的时候已经确定且无法改变。

4.小结

Python dict和tuple内存占用为何差这么多,原因不仅是上面提到的线性数组利用率的问题,还有线性数组存储的数据:tuple只是存储PyObject,但是dict要存储对应的hash值,key和value,这也是导致内存占用差距变大的原因。