Python одним щелчком мыши для пакета Jar, Java вызывает новую позицию Python!

Python

Поклонники и друзья, я не знаю, устали ли вы читать историю (если вам не надоело, вы можете оставить сообщение и рассказать мне ^_^), сегодня эта статья имеет другой вкус и серьезно относится к написание технических статей. Ближе к дому, приступим!

Структура этой статьи:

- 需求背景
  - 进击的 Python
  - Java 和 Python
- 给 Python 加速
  - 寻找方向
  - Jython?
- Python->Native 代码
  - 整体思路
  - 实际动手
  - 自动化
- 关键问题
  - import 的问题
  - Python GIL 问题
- 测试效果
- 总结

фон спроса

Атака на Python

Можно сказать, что с появлением искусственного интеллекта у Python, языка программирования, который когда-то был нишевым, появилась вторая весна.

Среды разработки машинного обучения/глубокого обучения, основанные на tensorflow, pytorch и т. д., очень популярны, продвигая python, язык программирования, который раньше был хорош для рептилий (не сердитесь на фанатов python), в рейтинге языков программирования TIOBE. Трон тройки лидеров, уступая только Java и C, сбил многих соперников, таких как C++, JavaScript, PHP и C#.

Конечно, Xuanyuanjun никогда не выступает за конкуренцию и сравнение языков программирования, у каждого языка есть свои преимущества и недостатки, а также своя область применения. С другой стороны, статистика TIOBE не может отражать реальную ситуацию в Китае, приведенный выше пример лишь отражает сегодняшнюю популярность языка Python.

Ява или Питон

Говоря о наших потребностях, в настоящее время на многих предприятиях есть как группы исследований и разработок Python, так и группы исследований и разработок Java.Команда Python отвечает за разработку алгоритмов искусственного интеллекта, а команда Java отвечает за разработку алгоритмов, предоставляя интерфейсы для возможностей алгоритмов. через инженерную упаковку.Используется приложениями более высокого уровня.

Вы можете спросить, почему бы просто не использовать Java для разработки ИИ? Чтобы получить две команды. Фактически, фреймворки, включая TensorFlow, постепенно начали поддерживать платформу Java, и нет ничего невозможного в том, чтобы использовать Java для разработки ИИ (на самом деле, многие команды уже делают это), но в силу исторических причин люди, которые занимаются разработкой ИИ, не Большинство из этих людей находятся в стеке технологий Python Экосистема разработки ИИ Python была относительно полной, поэтому во многих компаниях командам алгоритмов и командам инженеров приходится использовать разные языки.

Теперь пришло время задать важные вопросы этой статьи:Как команда инженеров Java использует алгоритмические возможности Python?

По сути, есть только один ответ:Python запускает веб-сервис через такие фреймворки, как Django/Flask, а Java взаимодействует с ним через Restful API.

Вышеупомянутый метод действительно может решить проблему, но вместе с ним возникает проблема с производительностью. Особенно после увеличения количества пользователей и большого количества одновременных обращений к интерфейсу доступ к сети и скорость выполнения кода Python станут узким местом, тормозящим весь проект.

Конечно, компании с хорошими деньгами могут использовать аппаратное обеспечение для наращивания производительности, а если нет, то развернуть еще несколько веб-сервисов на Python.

Кроме того, есть ли более доступное решение? Вот о чем эта статья.

Ускорить Python

найти направление

Среди упомянутых выше узких мест производительности есть две основные причины снижения скорости выполнения:

  • Доступ через сеть не такой быстрый, как прямой вызов внутреннего модуля
  • Python интерпретируется и выполняется не быстро

Как мы все знаем, Python — это интерпретируемый язык сценариев, вообще говоря, с точки зрения скорости выполнения:

Интерпретируемый язык

Естественно, есть два направления, над которыми мы должны работать:

  • Можно ли его вызвать напрямую локально без выхода в сеть?
  • Python не интерпретирует выполнение

Объединив два вышеуказанных пункта, наша цель также ясна:

Преобразование кода Python в модули, которые Java может напрямую вызывать в исходном коде.

Для Java существует два типа собственных вызовов:

  • Пакет кода Java
  • Модуль собственного кода

На самом деле то, что мы обычно называем Python, относится к CPython, интерпретатору, разработанному на языке C для интерпретации и выполнения. Кроме того, в дополнение к языку C многие другие языки программирования также могут разрабатывать виртуальные машины в соответствии со спецификацией языка Python для интерпретации и выполнения скриптов Python:

  • CPython: интерпретатор, написанный на C
  • Jython: интерпретатор, написанный на Java.
  • IronPython: интерпретатор для платформы .NET
  • PyPy: собственный интерпретатор Python (курица и яйцо, яйцо)

Jython?

Если сценарий Python может быть выполнен непосредственно в JVM, взаимодействие с бизнес-кодом Java, естественно, является самым простым. Но последующие исследования обнаружили, что дорогу быстро перекрыли:

  • Синтаксис Python 3.0 и выше не поддерживается.
  • Если сторонняя библиотека, указанная в исходном коде Python, содержит расширения языка C, она не будет предоставлять поддержку, например numpy и т. д.

Этот способ не работает, есть другой: конвертировать Python-код в Native-кодовые блоки, а Java-вызовы через интерфейс JNI.

Python -> собственный код

вся идея

Сначала преобразуйте исходный код Python в код C, затем используйте GCC для компиляции кода C в двоичный модуль so/dll, затем выполните пакет интерфейса Java Native, используйте команду упаковки Jar, чтобы преобразовать его в пакет Jar, а затем Java. можно вызвать напрямую.

Процесс не сложный, но для полной реализации поставленной цели есть ключевая проблема, которую необходимо решить:

Как преобразовать код Python в код C?

Наконец настала очередь главного героя этой статьи.Основной инструмент, который будет использоваться, называется:Cython

Обратите внимание, что здесьCythonи вышеупомянутыйCPythonНе то же самое. В узком смысле CPython относится к интерпретатору Python, написанному на языке C, который является интерпретатором сценариев Python по умолчанию в Windows и Linux.

Хотя Cython является сторонней библиотекой для Python, вы можете использоватьpip install Cythonустановить.

Официальное введение Cython — это расширенный набор спецификаций языка Python, который может преобразовывать смешанные скрипты Python + C .pyx в код C, в основном используемый для оптимизации производительности скриптов Python или вызова библиотек функций C из Python.

Это звучит немного сложно и немного запутанно, но это не имеет значения, просто поймите суть:Cython может преобразовывать скрипты Python в код C

Давайте посмотрим на эксперимент:

# FileName: test.py
def TestFunction():
  print("this is print from python script")

Преобразуйте приведенный выше код с помощью Cython, чтобы сгенерировать test.c, который выглядит так:

Код очень длинный и не удобный для чтения, здесь только скриншоты для иллюстрации.

руки вверх

1. Подготовьте исходный код Python

# FileName: Test.py
# 示例代码:将输入的字符串转变为大写
def logic(param):
  print('this is a logic function')
  print('param is [%s]' % param)
  return param.upper()

# 接口函数,导出给Java Native的接口
def JNI_API_TestFunction(param):
  print("enter JNI_API_test_function")
  result = logic(param)
  print("leave JNI_API_test_function")
  return result

注意1:Вот соглашение, используемое в исходном коде Python:以JNI_API_为前缀开头的函数表示为Python代码模块要导出对外调用的接口函数, цель этого состоит в том, чтобы позволить нашей системе пакетов Jar одним щелчком мыши автоматически определять, какие интерфейсы извлекать в качестве экспортных функций.

注意2:Вход интерфейсной функции этого типа — строка типа python str, и вывод тоже такой же, что удобно для портирования.JSONИнтерфейс RESTful, который принимает форму в качестве параметра. использоватьJSONПреимущество заключается в том, что параметры могут быть инкапсулированы для поддержки множества сложных форм параметров без перегрузки различных функций интерфейса для внешних вызовов.

注意3:Еще один момент, на который стоит обратить внимание, это то, что в префиксе функции интерфейсаJNI_API_позади,Имена функций не могут быть названы в обычной нотации подчеркивания Python, но должны использовать нотацию верблюжьего регистра.Обратите внимание, что это не предложение, а требование по причинам, которые будут упомянуты позже.

2. Подготовьте файл main.c

Функция этого файла состоит в том, чтобы инкапсулировать код, сгенерированный преобразованием Cython, и инкапсулировать его в стиль интерфейса Java JNI для следующего шага использования Java.

/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
#include <Python.h>
#include <stdio.h>

#ifndef _Included_main
#define _Included_main
#ifdef __cplusplus
extern "C" {
#endif

#if PY_MAJOR_VERSION < 3
# define MODINIT(name)  init ## name
#else
# define MODINIT(name)  PyInit_ ## name
#endif
PyMODINIT_FUNC  MODINIT(Test)(void);

JNIEXPORT void JNICALL Java_Test_initModule
(JNIEnv *env, jobject obj) {
  PyImport_AppendInittab("Test", MODINIT(Test));
  Py_Initialize();

  PyRun_SimpleString("import os");
  PyRun_SimpleString("__name__ = \"__main__\"");
  PyRun_SimpleString("import sys");
  PyRun_SimpleString("sys.path.append('./')");

  PyObject* m = PyInit_Test_Test();
  if (!PyModule_Check(m)) {
  	PyModuleDef *mdef = (PyModuleDef *) m;
  	PyObject *modname = PyUnicode_FromString("__main__");
  	m = NULL;
  	if (modname) {
  	  m = PyModule_NewObject(modname);
  	  Py_DECREF(modname);
  	  if (m) PyModule_ExecDef(m, mdef);
  	}
  }
  PyEval_InitThreads();
}


JNIEXPORT void JNICALL Java_Test_uninitModule
(JNIEnv *env, jobject obj) {
  Py_Finalize();
}

JNIEXPORT jstring JNICALL Java_Test_testFunction
(JNIEnv *env, jobject obj, jstring string)
{
  const char* param = (char*)(*env)->GetStringUTFChars(env, string, NULL);
  static PyObject *s_pmodule = NULL;
  static PyObject *s_pfunc = NULL;
  if (!s_pmodule || !s_pfunc) {
    s_pmodule = PyImport_ImportModule("Test");
    s_pfunc = PyObject_GetAttrString(s_pmodule, "JNI_API_testFunction");
  }
  PyObject *pyRet = PyObject_CallFunction(s_pfunc, "s", param);
  (*env)->ReleaseStringUTFChars(env, string, param);
  if (pyRet) {
    jstring retJstring = (*env)->NewStringUTF(env, PyUnicode_AsUTF8(pyRet));
    Py_DECREF(pyRet);
    return retJstring;
  } else {
    PyErr_Print();
    return (*env)->NewStringUTF(env, "error");
  }
}
#ifdef __cplusplus
}
#endif
#endif

В этом файле 3 функции:

  • Java_Test_initModule: инициализация python работает
  • Java_Test_uninitModule: деинициализация Python работает
  • Java_Test_testFunction: Настоящий бизнес-интерфейс инкапсулирует вызов функции JNI_API_testFuncion, определенной в исходном Python, а также отвечает за преобразование типа jstring параметра на уровне JNI.

Согласно спецификации интерфейса JNI имена функций C на собственном уровне должны соответствовать следующей форме:

// QualifiedClassName: 全类名
// MethodName: JNI接口函数名
void
JNICALL
Java_QualifiedClassName_MethodName(JNIEnv*, jobject);

Следовательно, определение должно быть названо, как указано выше, в файле main.c, поэтому подчеркивается, что имя функции интерфейса python нельзя подчеркивать, что приведет к тому, что интерфейс JNI не найдет соответствующую нативную функцию.

3. Используйте инструменты Cython для компиляции и создания динамических библиотек

Добавьте небольшую подготовительную работу: измените суффикс исходного файла Python с.pyизменить на.pyx

Файлы Test.pyx и main.c с исходным кодом Python готовы, следующий шагCythonКогда он выйдет на сцену, он автоматически преобразует все файлы pyx в файлы .c и объединит их с нашим собственным файлом main.c для внутреннего вызова gcc для создания файла динамической двоичной библиотеки.

Работа Cython требует подготовки файла setup.py и настройки информации о компиляции преобразования, включая входные файлы, выходные файлы, параметры компиляции, включаемые каталоги и каталоги ссылок, как показано ниже:

from distutils.core import setup
from Cython.Build import cythonize
from distutils.extension import Extension

sourcefiles = ['Test.pyx', 'main.c']

extensions = [Extension("libTest", sourcefiles, 
  include_dirs=['/Library/Java/JavaVirtualMachines/jdk1.8.0_191.jdk/Contents/Home/include',
    '/Library/Java/JavaVirtualMachines/jdk1.8.0_191.jdk/Contents/Home/include/darwin/',
    '/Library/Frameworks/Python.framework/Versions/3.6/include/python3.6m'],
  library_dirs=['/Library/Frameworks/Python.framework/Versions/3.6/lib/'],
  libraries=['python3.6m'])]

setup(ext_modules=cythonize(extensions, language_level = 3))

注意:Это включает в себя компиляцию двоичного кода Python, что требует компоновки библиотеки Python.

注意:Это включает в себя определение структур данных, связанных с JNI, которые должны включать каталог Java JNI.

После того, как файл setup.py будет готов, выполните следующую команду, чтобы начать преобразование + компиляцию:

python3.6 setup.py build_ext --inplace

Сгенерируем нужные нам файлы динамической библиотеки:libTest.so

4. Подготовьте файл интерфейса для вызовов Java JNI.

Использование бизнес-кода Java должно определять интерфейс следующим образом:

// FileName: Test.java
public class Test {
  public native void initModule();
  public native void uninitModule();
  public native String testFunction(String param);
}

На данный момент цель вызова в Java достигнута.Обратите внимание, что перед вызовом бизнес-интерфейса вам необходимо вызвать initModule для выполнения инициализации Python на нативном уровне.


import Test;
public class Demo {
    public void main(String[] args) {
        System.load("libTest.so");
        Test tester = new Test();
        tester.initModule();
        String result = tester.testFunction("this is called from java");
        tester.uninitModule();

        System.out.println(result);
    }
}

вывод:

enter JNI_API_test_function
this is a logic function
param is [this is called from java]
leave JNI_API_test_function
THIS IS CALLED FROM JAVA!

Успешно реализован вызов кода Python на Java!

5. Пакет в виде пакета Jar

Недостаточно сделать вышеперечисленное, для лучшего взаимодействия с пользователем мы сделаем еще один шаг и упакуем его в пакет Jar.

Прежде всего, необходимо расширить исходный файл интерфейса JNI, а также добавить статический метод loadLibrary для автоматической реализации выпуска и загрузки файла so.

// FileName: Test.java
public class Test {
  public native void initModule();
  public native void uninitModule();
  public native String testFunction(String param);
  public synchronized static void loadLibrary() throws IOException {
    // 实现略...
  }
}

Затем преобразуйте приведенный выше файл интерфейса в файл класса Java:

javac Test.java

Наконец, подготовьтесь разместить файл класса и так далее в каталоге Test и упаковать его:

jar -cvf Test.jar ./Test

автоматизация

Вышеупомянутые 5 шагов действительно хлопотны, если вам приходится делать это каждый раз вручную! К счастью, мы можем написать скрипты Python, чтобы полностью автоматизировать этот процесс и действительно сделать это.Python一键转换Jar包

Из-за нехватки места здесь упомянуты только ключевые моменты процесса автоматизации:

  • Автоматически сканировать и извлекать функции интерфейса, которые необходимо экспортировать в исходный код Python.
  • Java-файлы интерфейса Main.c, setup.py и JNI должны генерироваться автоматически (вы можете определить шаблоны + параметры для быстрой сборки), вам необходимо иметь дело с соответствующими отношениями между именами модулей и именами функций.

ключевой вопрос

1. Проблема с импортом

Продемонстрированный выше случай — это всего лишь один py-файл, но на практике наши проекты обычно имеют несколько py-файлов, и эти файлы обычно образуют сложную иерархию каталогов с различными отношениями импорта между собой, которые являются запутанными.

Одна из самых больших ловушек инструмента Cython:Код файла, обработанный им, потеряет информацию уровня каталога файла кода.Как показано на рисунке ниже, нет никакой разницы между кодом, преобразованным C.py, и кодом, сгенерированным m/C.py.

Это приводит к очень большой проблеме: если есть ссылка на модуль C.py в каталоге m в коде A.py или B.py, потеря информации о каталоге заставит оба сообщать об ошибке при выполнении. import mC, и соответствующий модуль не может быть найден.

К счастью, эксперименты показали, что на приведенном выше рисунке, если три модуля A, B и C находятся в одном каталоге, импорт может быть выполнен правильно.

Сюаньюань Цзюнь однажды попытался прочитать исходный код Cython и модифицировал его, чтобы сохранить информацию о каталоге, чтобы сгенерированный код C все еще можно было нормально импортировать, но из-за спешки времени и недостаточного понимания механизма интерпретатора Python. , он выбрал после некоторых попыток.

Я долго застревал на этой проблеме, и в итоге выбрал глупый путь:Разверните каталог иерархии кода в виде дерева в плоскую структуру каталогов., для примера на рисунке выше расширенная структура каталогов становится

A.py
B.py
m_C.py

Этого мало, надо еще все ссылки на C в A и B исправить на ссылки на m_C.

Это выглядит просто, но на деле все намного сложнее.В Python импорт не так прост, как импорт, существуют различные сложные формы:

import package
import module
import package.module
import module.class / function
import package.module.class / function
import package.*
import module.*
from module import *
from module import module
from package import *
from package import module
from package.module import class / function
...

Кроме того, также может существовать способ прямой ссылки через модуль в коде.

Цена развертывания в плоскую структуру - справиться со всем вышеперечисленным! У Сюаньюань-цзюня не было выбора, кроме как прибегнуть к этому последнему средству. Если у вас, ребята, есть лучшее решение, пожалуйста, не стесняйтесь просветить меня.

2. Проблема Python GIL

Конвертированный jar Python начали использовать в продакшене, но потом обнаружилась проблема:

Всякий раз, когда счетчик параллелизма Java увеличивается, JVM всегда время от времени появляется сбой.

Последующий анализ информации о сбое показал, что сбой произошел в коде, преобразованном Python, в собственном коде.

  • Это ошибка Cython?
  • Есть ли ямка в преобразованном коде?
  • Или что-то не так с приведенным выше исправлением импорта?

Темная туча краха долго висит над моей головой, успокойся и подумай: Почему при тестировании проблем не обнаружено, а вылетает только после выхода в интернет?

Еще раз взглянув на краш-лог, я обнаружил, что в нативном коде место, где возникает исключение, всегда находится там, где malloc выделяет память.Может быть, память повреждена? Выяснилось, что во время теста выполнялись только функциональные тесты, а параллельные стресс-тесты не проводились, а сценарии сбоев всегда были в мультипараллельной среде. Чтобы получить доступ к интерфейсу JNI из нескольких потоков, собственный код будет выполняться в контексте нескольких потоков.

Внезапное предупреждение:99% имеет какое-то отношение к блокировке Python GIL!

Как мы все знаем, в силу исторических причин, Python родился в 1990-х.В то время концепция многопоточности была далеко не так популярна, как сегодня.Как продукт этой эпохи, Python родился как однопоточный продукт.

Хотя Python также имеет многопоточную библиотеку, которая позволяет создавать несколько потоков, поскольку версия интерпретатора на языке C не является потокобезопасной в управлении памятью, внутри интерпретатора есть очень важная блокировка, которая ограничивает многопоточность Python, так что так называемая многопоточность на самом деле просто состоит в том, что все по очереди занимают яму.

Получается, что GIL планируется и управляется интерпретатором.Теперь, когда он преобразован в код C, кто отвечает за управление безопасностью многопоточности?

Поскольку Python предоставляет набор интерфейсов для вызовов языка C, что позволяет выполнять сценарии Python в программах на C, поэтому просмотрите документацию этого API, чтобы узнать, сможете ли вы найти ответ.

К счастью, я действительно нашел это:

Получите замок GIL:

Снимите блокировку GIL:

Блокировка GIL должна быть получена при входе вызова JNI, а блокировка GIL должна быть снята при выходе из интерфейса.

После добавления управления замком GIL наконец-то решена надоедливая проблема с Crash!

Тестовый эффект

Подготовьте два идентичных файла py, одну и ту же функцию алгоритма, доступ к одному осуществляется через веб-интерфейс Flask (веб-служба развертывается локально на 127.0.0.1 для минимизации задержки в сети), а другой преобразуется в пакет Jar с помощью описанного выше процесса.

В сервисе Java два интерфейса вызываются 100 раз соответственно, и вся работа по тестированию выполняется 10 раз, а время выполнения рассчитывается следующим образом:

В приведенном выше тесте, чтобы еще больше отличить задержку, вызванную сетью, и задержку выполнения самого кода, вход и выход функции алгоритма замеряются по времени, а также время делается до вызова интерфейса выполнения Java и запуска. место, где получен результат, чтобы его можно было вычислить Доля времени выполнения алгоритма во всем процессе вызова интерфейса.

  • Из результатов видно, что через доступ к интерфейсу, выполняемый веб-API, время выполнения самого алгоритма составляет только 30%+, и большая часть времени тратится на сетевые накладные расходы (отправка и получение пакетов данных, планирование обработка фреймворка Flask и др.).

  • Через локальный вызов интерфейса JNI время выполнения алгоритма составляет более 80% времени выполнения всего интерфейса, в то время как процесс преобразования интерфейса Java JNI занимает всего 10%+ времени, что эффективно улучшает эффективность и сокращает потери дополнительного времени.

  • Кроме того, просто глядя на часть выполнения самого алгоритма, время выполнения того же кода после преобразования в собственный код составляет 300–500 мкс, в то время как время интерпретации и выполнения CPython составляет 2000–4000 мкс, что также сильно отличается.

Суммировать

В этой статье представлен новый способ вызова кода Python из Java. Он предназначен только для справки. Его зрелость и стабильность все еще обсуждаются. Доступ через интерфейс HTTP Restful по-прежнему является первым выбором для межъязыковой стыковки.

Что касается метода в тексте, заинтересованные друзья могут оставить сообщение для обмена.

PS:В связи с ограниченным уровнем автора, если есть ошибки в тексте, прошу сообщить мне, дабы не вводить читателей в заблуждение, спасибо.

Прошлый популярный обзор

Воспоминания об объекте Java: сборка мусора

Kernel Address Space Adventure 3: Управление привилегиями

Кто переместил ваш HTTPS-трафик?

Рекламные секреты в роутерах

Kernel Address Space Adventure 2: прерывания и исключения

DDoS-атака: Война Бесконечности

Шокирующий случай, вызванный SQL-инъекцией

Космические приключения ядра: системные вызовы

Фантастическое путешествие HTTP-пакета

Захватывающее путешествие DNS-пакета

Я мошеннический поток программного обеспечения

Отсканируйте код, чтобы следовать, более захватывающим