Kivy A to Z -- 通过绑定进程运行CPU提高Python程序在多核CPU平台上的性能

时间:2023-02-04 08:04:25

 1. 从Python GIL系列文章中我们已经对Python的GIL有了一个比较清醒的认识

2. 要提高Python程序在多核CPU情况下的性能,除了使用进程替代线程外,一个更为实用的方法就是绑定Python进程运行于指定CPU。

3. 接下来看下如何在Kivy中做到这一点

4. 修改src/jni/application/python/start.c

#define PY_SSIZE_T_CLEAN

#include "Python.h"

#ifndef Py_PYTHON_H

#error Python headers needed to compile C extensions, please install development version of Python.

#else

#include <stdio.h>

#include <stdlib.h>

#include <unistd.h>

#include <jni.h>

#include "SDL.h"

#include "android/log.h"

#include "jniwrapperstuff.h"

#define LOG(x) __android_log_write(ANDROID_LOG_INFO, "python", (x))

#define _GNU_SOURCE

#include <sched.h>

#include <linux/unistd.h>

typedef unsigned int cpu_set_t;

static PyObject *androidembed_log(PyObject *self, PyObject *args) {

char *logstr = NULL;

if (!PyArg_ParseTuple(args, "s", &logstr)) {

return NULL;

}

LOG(logstr);

Py_RETURN_NONE;

}

static int

sched_setaffinity(pid_t pid, size_t len, cpu_set_t const * cpusetp)

{

return syscall(__NR_sched_setaffinity, pid, len, cpusetp);

}

static int

sched_getaffinity(pid_t pid, size_t len, cpu_set_t const * cpusetp)

{

return syscall(__NR_sched_getaffinity, pid, len, cpusetp);

}

static PyObject *

get_process_affinity_mask(PyObject *self, PyObject *args)

{

unsigned long cur_mask;

unsigned int len = sizeof(cur_mask);

pid_t pid;

if (!PyArg_ParseTuple(args, "i:get_process_affinity_mask", &pid))

return NULL;

if (sched_getaffinity(pid, len,

(cpu_set_t *)&cur_mask) < 0) {

PyErr_SetFromErrno(PyExc_ValueError);

return NULL;

}

return Py_BuildValue("l", cur_mask);

}

static PyObject *

set_process_affinity_mask(PyObject *self, PyObject *args)

{

unsigned long new_mask;

unsigned long cur_mask;

unsigned int len = sizeof(new_mask);

pid_t pid;

if (!PyArg_ParseTuple(args, "il:set_process_affinity_mask", &pid, &new_mask))

return NULL;

if (sched_getaffinity(pid, len,

(cpu_set_t *)&cur_mask) < 0) {

PyErr_SetFromErrno(PyExc_ValueError);

return NULL;

}

if (sched_setaffinity(pid, len, (cpu_set_t *)&new_mask)) {

PyErr_SetFromErrno(PyExc_ValueError);

return NULL;

}

return Py_BuildValue("l", cur_mask);

}

static PyMethodDef AndroidEmbedMethods[] = {

{"log", androidembed_log, METH_VARARGS,"Log on android platform"},

{"get_process_affinity_mask", get_process_affinity_mask, METH_VARARGS,"get_process_affinity_mask"},

{"set_process_affinity_mask", set_process_affinity_mask, METH_VARARGS,"set_process_affinity_mask"},

{NULL, NULL, 0, NULL}

};

PyMODINIT_FUNC initandroidembed(void) {

(void) Py_InitModule("androidembed", AndroidEmbedMethods);

}

int file_exists(const char * filename)

{

FILE *file;

if (file = fopen(filename, "r")) {

fclose(file);

return 1;

}

return 0;

}

int main(int argc, char **argv) {

char *env_argument = NULL;

int ret = 0;

FILE *fd;

LOG("Initialize Python for Android");

env_argument = getenv("ANDROID_ARGUMENT");

setenv("ANDROID_APP_PATH", env_argument, 1);

//setenv("PYTHONVERBOSE", "2", 1);

Py_SetProgramName(argv[0]);

Py_Initialize();

PySys_SetArgv(argc, argv);

/* ensure threads will work.

*/

PyEval_InitThreads();

/* our logging module for android

*/

initandroidembed();

/* inject our bootstrap code to redirect python stdin/stdout

* replace sys.path with our path

*/

PyRun_SimpleString(

"import sys, posix\n" \

"private = posix.environ['ANDROID_PRIVATE']\n" \

"argument = posix.environ['ANDROID_ARGUMENT']\n" \

"sys.path[:] = [ \n" \

" private + '/lib/python27.zip', \n" \

" private + '/lib/python2.7/', \n" \

" private + '/lib/python2.7/lib-dynload/', \n" \

" private + '/lib/python2.7/site-packages/', \n" \

" argument ]\n" \

"import androidembed\n" \

"class LogFile(object):\n" \

" def __init__(self):\n" \

" self.buffer = ''\n" \

" def write(self, s):\n" \

" s = self.buffer + s\n" \

" lines = s.split(\"\\n\")\n" \

" for l in lines[:-1]:\n" \

" androidembed.log(l)\n" \

" self.buffer = lines[-1]\n" \

" def flush(self):\n" \

" return\n" \

"sys.stdout = sys.stderr = LogFile()\n" \

"import site; print site.getsitepackages()\n"\

"print 'Android path', sys.path\n" \

"print 'Android kivy bootstrap done. __name__ is', __name__");

/* run it !

*/

LOG("Run user program, change dir and execute main.py");

chdir(env_argument);

/* search the initial main.py

*/

char *main_py = "main.pyo";

if ( file_exists(main_py) == 0 ) {

if ( file_exists("main.py") )

main_py = "main.py";

else

main_py = NULL;

}

if ( main_py == NULL ) {

LOG("No main.pyo / main.py found.");

return -1;

}

fd = fopen(main_py, "r");

if ( fd == NULL ) {

LOG("Open the main.py(o) failed");

return -1;

}

/* run python !

*/

ret = PyRun_SimpleFile(fd, main_py);

if (PyErr_Occurred() != NULL) {

ret = 1;

PyErr_Print(); /* This exits with the right code if SystemExit. */

if (Py_FlushLine())

PyErr_Clear();

}

/* close everything

*/

Py_Finalize();

fclose(fd);

LOG("Python for android ended.");

return ret;

}

JNIEXPORT void JNICALL JAVA_EXPORT_NAME(PythonService_nativeStart) ( JNIEnv* env, jobject thiz,

jstring j_android_private,

jstring j_android_argument,

jstring j_python_home,

jstring j_python_path,

jstring j_arg )

{

jboolean iscopy;

const char *android_private = (*env)->GetStringUTFChars(env, j_android_private, &iscopy);

const char *android_argument = (*env)->GetStringUTFChars(env, j_android_argument, &iscopy);

const char *python_home = (*env)->GetStringUTFChars(env, j_python_home, &iscopy);

const char *python_path = (*env)->GetStringUTFChars(env, j_python_path, &iscopy);

const char *arg = (*env)->GetStringUTFChars(env, j_arg, &iscopy);

setenv("ANDROID_PRIVATE", android_private, 1);

setenv("ANDROID_ARGUMENT", android_argument, 1);

setenv("PYTHONOPTIMIZE", "2", 1);

setenv("PYTHONHOME", python_home, 1);

setenv("PYTHONPATH", python_path, 1);

setenv("PYTHON_SERVICE_ARGUMENT", arg, 1);

char *argv[] = { "service" };

/* ANDROID_ARGUMENT points to service subdir,

* so main() will run main.py from this dir

*/

main(1, argv);

}

#endif


 

相比原来的代码添加了两个python接口:

 get_process_affinity_mask

 set_process_affinity_mask

还有由于sched_setaffinity,sched_getaffinity这两个函数在 在ndk中没有定义,所以参考网上的资料自己实现了下。

注意unistd.h不要包含出错了,ndk中有好几个unistd.h,应该是linux下的unistd.h,要不然传统编译出错。

 

5. 编译,具体的可参考《Kivy的编译环境的搭建以及编译和运行》一文

 

5.1先设置环境变量,

export ANDROIDSDK="/path/to/android/android-sdk-linux_86"

export ANDROIDNDK="/path/to/android/android-ndk-r8c"

export ANDROIDNDKVER=r8c

export ANDROIDAPI=14

export PATH=$PATH:/mnt/develop/android_dev/kivy/apache-ant-1.9.3/bin:$ANDROIDNDK

  

5.2 在python-for-android目录执行:

./distribute.sh -m 'openssl pyjnius pil kivy'

 

6. 使用方法:在代码初始化时加上

 import androidembed;androidembed.set_process_affinity_mask(0,1)

7. 打包python程序到apk 

../../build/hostpython/Python-2.7.2/hostpython build.py --package org.test.touchtracer --name touchtracer --version 1.0 --dir ../../build/kivy/kivy-stable/examples/demo/touchtracer debug

8. 但是在实际的手机上测试时,Python在多线程情况下即使不绑定CPU,也不会造成性能上的的损失,看来在arm平台上和intel平台上还真不一样,但是总之有备无患吧。

9. 参考资料

 https://code.google.com/p/android/issues/detail?id=19851