月度归档: 2020 年 10 月

  • Boost Python的C++对象, Pickle支持及其原理

    默认用boost python包裹的C++对象是不支持pickle的,如果要用pickle.dumps(obj)的话那会提示错误

    Pickling of "xxx" instances is not enabled.

    这边吐槽一下在最新的代码里,给的reference链接其实还是不可用的。真正正确的是https://www.boost.org/doc/libs/1_74_0/libs/python/doc/html/reference/topics/pickle_support.html

    让你的class支持Pickle协议

    若要让你的C++ Class支持Pickle协议,比较“正统”的方法是利用boost提供的boost::python::pickle_suite. 拿代码说话:

    struct world_t {
        world_t(const string& country) { ... }
    };
    
    struct world_pickle_suite : boost::python::pickle_suite
      {
        static
        boost::python::tuple
        getinitargs(const world_t& w)
        {
          // [可选实现] 返回一个boost::python::tuple元组,其值被用来构造
          // 如果一个类的构造函数**不需要参数**的话,可以不用重载这个方法。
          return boost::python::make_tuple(w.country());
        }
    
        static
        boost::python::tuple
        getstate(const world_t& w)
        {
          // [可选实现] 如果对象的构造函数并不能完全恢复对象的状态,
          // 那么要用此函数返回其状态值
        }
    
        static
        void
        setstate(world_t& w, boost::python::tuple state)
        {
          // [可选实现] 如果对象的构造函数并不能完全恢复对象的状态,
          // 那么要用此函数把state里的状态恢复到w
        }
      };
    
    boost::python::class_<world_t>("world", args<const std::string&>())
      // 定义world_t的boost python包装类
      .def_pickle(world_pickle_suite());
      // ...
    

    需要注意的是,如果在suite里定义了getstate/setstate并且这个类的__dict__属性非空,boost是会报错的。一种可能的情况是你包装的类是一个子类,这时候还需要自己实现__getstate_manages_dict__属性。这边不赘述,可参考这里

    原理简释

    总所周知,Python3里若要在自定义的类里实现可Pickle,至少需要:

    • 它的__dict__属性是可pickle的,或;
    • 调用__getstate__()拿到的返回值是可pickle的,随后会调用__setstate__()方法在unpickle的时候恢复状态.
      但在这些函数的背后,再稍微底层一点其实是通过__reduce__()方法实现的(更严格一点,它会先去寻找__reduce_ex__()是否可用)。这个方法简单来说就是返回一个tuple, 这个tuple有从构建对象到恢复状态所需的各种元素.

    In fact, these methods are part of the copy protocol which implements the __reduce__() special method.
    https://docs.python.org/3/library/pickle.html

    所以boost::python其实自己实现了一个对象的__reduce__()方法,在src/object/pickle_support.cpp里(源文件在这)。主要做了几件事

    1. 拿到当前对象的__class__属性(也就是class object);
    2. 检查对象是否有__safe_for_unpickling__属性。如果没有的话,就是本文最早提到的报错了;
    3. 检查对象是否有__getinitargs__()方法,若有则取值。没有的话,unpickle的时候按照无参数构造来处理;
    4. 检查对象是否有__getstate__()方法,若有则取值;
    5. 检查对象是否有__dict__属性,若有则会检查是否有__getstate_manages_dict__属性并获取这两个属性的值。
    6. 返回tuple(),内容依次为1-5里拿到的值。

    可以看到,这个__reduce__()方法是遵循了Python里的object.__reduce__()协定的。当然了,如果某些使用者觉得继承一个suite类都觉得麻烦或者达不到自己的需求,也可以通过其他手段完成,只要记得自己整一个__reduce__()方法,只需满足Python的协定即可.

    class_obj.attr("__reduce__") = boost::python::object(/*YOUR REDUCE FUNCTION*/);

    再深一点~

    如果你跟我一样无聊,那就来看看cpython自己想要__reduce__()的时候是怎么用到__getstate__()的吧.

    cpython/Objects/typeobject.c这边有个函数叫

    static PyObject* _common_reduce(PyObject *self, int proto)

    ,在pickle protocol>2时会调用`static PyObject *
    reduce_newobj(PyObject *obj)`方法,这个方法是Python里大多数默认对象的默认__reduce__逻辑。其中有一句

    state = _PyObject_GetState(obj,
                    !hasargs && !PyList_Check(obj) && !PyDict_Check(obj));

    _PyObject_GetState里大致是从__dict__或者__slots__属性去获取对象的状态。

    这边再往上一些还能看到有调用_PyObject_GetNewArguments()的,里面的逻辑就会去拿__getnewargs_ex__或是__getnewargs__属性的值作为__new__一个新对象时候的传入参数。这两个函数是Pickle协议文档里介绍的四个推荐覆盖的“高阶”函数之一。与之对应,Python并不希望一般人去自己写一个__reduce__()出来.

    看到这里,再回想一下boost::python,pickle_suite里面的getinitargs()getstate()就分别对应到__getnewargs__()__getstate__()

  • Apache2配置微信公众号的反向代理

    题外话

    迫于妹子生日要到了,今年手头又比较紧,所以打算做个微信公众号的小东西骗骗她😜
    目前跑网站的服务器虽然配置不咋地,但是服务器上也就一个typecho的php项目要跑,平时访问量也门可罗雀,所以打算利用原有的服务器搭个反向代理到微信公众号的服务端。

    目标

    主域名example.com:

    • 80/443端口直接到原有的网站

    子域名wechat.example.com

    • 80端口反向代理到本地服务http://localhost:8766/

    实作

    其实很简单,新建一个VirtualHost即可。
    创建一个/etc/apache2/sites-available/wechat.conf内容如下:

    <VirtualHost *:80>
            ServerName wechat.example.com
    
            ServerAdmin webmaster@localhost
    
            ProxyPass / http://localhost:8766/
            ProxyPassReverse / http://localhost:8766/
        
    
            ErrorLog ${APACHE_LOG_DIR}/error.log
            CustomLog ${APACHE_LOG_DIR}/access.log combined
    </VirtualHost>
    

    其中的关键是ProxyPass以及ProxyPassReverse.

    完成之后,用a2ensite wechat启用这个VirtualHost, 再systemctl reload apache2便万事大吉。

  • 记录一下找了半天的huge page坑——fork越来越慢的原因

    背景

    之前发现Jupyter Notebook下面,如果数据占用多的话,开多进程池会特别的慢。一开始以为是Python的锅,但是把multiprocessing.pool改成直接用os.fork()调用以后,问题依旧。照理来说unix下面使用fork开进程,会启用copy-on-write机制,内存增长并不是特别明显,但是实际在htop下面看内存仍然会在fork之后增长,并且和进程数量是线性相关的。

    原因

    随后想了老半天,想到了可能和页表有关系。查了一下,跑的服务器上huge page确实被禁用了(不知为何…).

    fork的机制简单地说,是在创建新进程的时候把老的进程控制块(Process Control Block)里内存页表拷贝给了新的PCB——这边具体内存的信息是不拷贝的。由于当时Notebook跑的数据处理任务,里面已经用了不少内存(100GB+),所以拷贝的时候如果用默认的4KB内存页,将会有100 * 1024 * 1024 / 4 = 104,857,600个页表! 按典型一个页表项(Page Table Entry)大小4Bytes计算,一个进程开出来光页表会耗400MB内存.