linux使用c语言,linux进行c语言编译

  linux使用c语言,linux进行c语言编译

  1后台查询理解(QU)是美团搜索的核心模块。它的主要职责是理解用户查询,生成查询意图、组件、重写等基本信号。并将它们应用到搜索的召回、排序、展示等环节,这对基础搜索体验至关重要。服务的在线主程序是基于C语言开发的,服务中会加载大量的词汇数据和预测模型。这些数据和模型的线下制作过程,有很多文本解析能力需要和线上服务保持一致,这样才能保证效果层面的一致性,比如文本归一化、分词等。

  这些离线生产流程通常用Python和Java实现。如果一个文案在线上线下用不同语言开发,很难保持策略和效果的统一。同时,这些能力还会不断迭代。在这种动态场景下,不断维护多语言版本的效果是平坦的,这给我们的日常迭代带来了很大的成本。因此,我们尝试通过跨语言调用动态链接库技术来解决这个问题,即开发一个基于C的so,通过不同语言的链路层将其封装成不同语言的构件库,并放入相应的生成流程中。这个方案的优点是显而易见的。主体的业务逻辑只需要开发一次,封装层只需要少量代码,主体业务迭代升级,其他语言几乎不需要改动。它只需要包含最新的动态链接库,发布最新的版本。同时,C作为一种底层语言,在很多场景下具有更高的计算效率和更高的硬件资源利用率,也给我们带来了一些性能优势。

  本文对我们在实际生产中尝试这种技术方案时遇到的问题和一些实践经验进行了完整的梳理,希望能给大家提供一些参考或帮助。

  2方案概述为了达到业务方开箱即用的目的,同时考虑到C、Python、Java用户的使用习惯,我们设计了以下协作结构:

  图1

  3实现细节Python和Java支持调用C接口,不支持调用C接口。所以用C语言实现的接口必须转换成C语言。为了不修改原C代码,C接口的上层用C语言封装一次,通常称为“胶水代码”。具体方案如下图所示:

  图2

  本章各部分如下:

  在[功能代码]部分,通过打印字符串的例子来描述各个语言部分的编码工作。【打包发布】部分介绍了如何将生成的动态库用Python和Java代码打包成资源文件发布到仓库,降低用户的访问成本。在[业务使用]部分,介绍了一个开箱即用的使用示例。在【易用性优化】部分,结合实际使用中遇到的问题,讲述了如何处理Python版本兼容性和动态库依赖。3.1功能代码3.1.1以C代码为例,实现打印一个字符串的功能。为了模拟实际的工业场景,编译了以下代码,分别生成动态库libstr_print_cpp.so和静态库libstr_print_cpp.a。

  str_print.h

  #pragma once#include string类str print { public:void print(const STD:string text);};str_print.cpp

  # include iostream # include str _ print . h void str print:print(const STD:string text){ STD:cout text STD:endl;}3.1.2 c_wrapper代码如上所述,需要封装C库,将其转换成提供外部C语言格式的接口。

  c _包装器. cpp

  # include str _ print . h extern C“{ void str _ print(const char * text){ str print CPP _ ins;STD:string str=text;CPP _ ins . print(str);}}3.1.3生成动态库为了支持Python和Java之间的跨语言调用,我们需要为封装的接口生成动态库。有三种方法可以生成动态库

  方法一:源代码依赖,c_wrapper和C代码一起编译生成libstr_print.so这样业务端只需要依赖一个so,成本低,但是需要获取源代码。对于一些现成的动态库,可能不适用。g-o libstr _ print . sostr _ print . cppc _ wrapper . CPP-fpic-共享模式二:动态链接模式。这种模式生成的libstr _ print.so在发布时需要携带其依赖库libstr_print_cpp.so。这样业务端需要同时依赖两个so,使用成本比较高,但是不需要提供原动态库的源代码。g-o libstr_print.so c _ wrapper . CPP-fpic-shared-l .-lstr _ print _ CPP模式三:静态链接模式,该模式生成的libstr _ print . so在发布时不需要携带libstr_print_cpp.so。这样业务端只需要依赖一个so,不是依赖源代码,而是需要提供一个静态库。g _ wrapper . cplibstr _ print _ CPP . a-fpic-shared-o libstr _ print .所以以上三种方法都有各自的适用场景和优缺点。在我们目前的业务场景中,由于工具库和包库都是自己开发的,并且可以获取源代码,所以业务端更容易依赖第一种方法。

  3.1.4 Python标准库自带的Python访问代码ctypes可以实现加载c的动态库的功能,使用方法如下:

  str_print.py

  #-*-编码:utf-8-*-importctypes # load CLI blib=ctypes . cdll . loadlibrary(。/libstr _ print.so) #接口参数类型映射lib . str _ print . arg types=[ctypes . c _ char _ p]lib . str _ print . restype=None #调用接口lib . str _ print( Hello World )LoadLibrary会返回一个指向动态库的实例,通过它可以直接调用Python中库中的函数。Argtypes和restype是动态库中函数的参数属性。前者是ctypes类型的列表或元组,用于指定动态库中函数接口的参数类型,后者是函数的返回类型(默认为c_int,可以省略,对于非c_int类型,需要显示规范)。这部分涉及到的参数类型映射,以及如何将struct、pointer等高级类型传入函数,可以参考附录中的文档。

  3.1.5 Java访问代码Java调用C lib有两种方式:JNI和JNA。从方便的角度来看,更推荐JNA。

  3.1.5.1 JNI access Java从1.1版本开始支持JNI接口协议,用于实现Java语言调用C/C动态库。在JNI模式下,前面提到的c_wrapper模块不再适用,JNI协议本身提供了适配层的接口定义,需要按照这个定义来实现。JNI模式的具体访问步骤如下:

  在Java代码中,native关键字被添加到需要跨语言调用的方法中,以声明它是一个本地方法。

  导入Java . lang . string;public类jn idemo { public native void print(String text);}通过javah命令,将代码中的原生方法生成到C语言对应的头文件中。这个头文件类似于前面提到的c_wrapper。

  javah JniDemo得到的头文件如下(为了节省篇幅,这里简化了一些注释和宏):

  # include JNI . h # ifdef _ _ cplusplusextern C { # endifJNIEXPORT void JNI call Java _ jn idemo _ print(JNI env *,jobject,jstring);#ifdef __cplusplus}#endifjni.h是JDK提供的,定义了调用Java和C语言所必须的相关实现。

  JNIEXPORT和JNICALL是JNI定义的两个宏。JNIEXPORT标识了支持在外部程序代码中调用动态库的方法,JNICALL定义了调用函数时参数的入栈和出栈约定。

  Java_JniDemo_print是自动生成的函数名,其格式是固定的。它由Java_{className}_{methodName}组成。JNI将根据这个约定注册Java方法和C函数之间的映射。

  在这三个参数中,前两个是固定的。JNIEnv在jni.h中封装了一些工具方法,jobject指向Java中的调用类,也就是JniDemo,通过它可以在c的堆栈中找到Java的类中成员变量的副本,JString指向传入的参数文本,这是Java中字符串类型的映射。类型映射的具体内容后面会详细展开。

  实现Java_JniDemo_print方法。

  JniDemo.cpp

  # include string # include JNI demo . h # include str _ print . h JNI export void JNI call Java _ JNI demo _ print(JNI env * env,jobject obj,jstring text){ char * str=(char *)env-GetStringUTFChars(text,JNI _ FALSE);STD:string tmp=str;StrPrint insins . print(tmp);}编译生成动态库。

  g-o libjnidemo . sojnidemo . CPP str _ print . CPP-fpic-shared-I $ Java _ home/include/-I $ Java _ home/include/Linux编译运行。

  Java-djava。library . path=path _ to _ libjn idemo . sojnidemojni机制通过一层C/C桥接实现跨语言调用协议。该函数广泛应用于Android系统中一些与图形计算相关的Java程序中。一方面可以通过Java调用大量的操作系统底层库,大大减少了JDK上驱动开发的工作量,另一方面可以充分利用硬件性能。不过,从3.1.5.1的描述中也可以看出,JNI本身的实施成本还是比较高的。特别是,在处理复杂类型的参数传递时,桥接层的C/C代码开发起来非常昂贵。为了优化这个过程,Sun公司领导了JNA(Java Native Access)开源项目。

  3.1.5.2 JNA Access JNA是一个基于JNI的编程框架,提供了一个C语言的动态转发器,实现了从Java类型到C类型的自动转换。因此,Java开发人员只需要在一个Java接口中描述目标原生库的功能和结构,不再需要编写任何原生/JNI代码,大大降低了Java调用本地共享库的开发难度。

  JNA的用法如下:

  将JNA库引入Java项目。

  DependencyGroupidcom.sun.jna/groupidArtifact idjna/Artifact id version 5 . 4 . 0/version/dependency声明动态库对应的Java接口类。

  公共接口CLibrary扩展库{ void str _ print(String text);//方法名与动态库的接口一致,参数类型需要用Java中的类型表示。执行过程中会做类型映射,原理介绍章节会详细讲解。}加载动态链接库,实现接口方法。

  JnaDemo.java

  包com . jna . demo;导入com . sun . jna . library;导入com . sun . jna . native;公立学校图书馆;公共接口CLibrary扩展库{ void str _ print(String text);} public jna demo(){ CLI brary=native . load( str _ print ,CLI brary . class);} public void str _ print(String text){ CLI brary . str _ print(text);}}相比JNI,JNA不再需要指定native关键字,生成C代码的JNI部分,做参数类型转换,大大提高了调用动态库的效率。

  3.2封装发布为了做到开箱即用,我们将动态库封装了相应的语言代码,并自动准备相应的依赖环境。这样用户只需要安装相应的库并引入到项目中,就可以直接开始调用了。这里需要说明的是,我们并没有把so发布到运行机,而是和接口代码一起发布到代码仓库。原因是我们开发的工具代码可能会被不同业务、不同背景(非C)的团队使用,所以我们无法保证所有业务团队都使用统一规范的运行环境,也无法做到如此统一的发布和更新。

  3.2.1 Python包发布Python可以通过setuptools将工具库打包,发布到pypi公共仓库。具体操作方法如下:

  创建一个目录。 MANIFEST.in #指定静态依赖 setup.py #代码用于发布strprint #工具库-_ init _的配置-source目录。py #工具包入口-libstr _ print.so

  # -*-编码:utf-8-*-import ctypes import OS import sys dirname,_=OS . path . split(OS . path . ABS path(_ _ file _ _))lib=ctypes . cdll . loadlibrary(dirname /libstr _ print . so )lib . str _ print . arg types=[ctypes . c _ char _ p]lib . str _ print . restype=none def str _ print(text):lib . str _ print(text)Write setup . py

  从下载导入设置,find _ packages setup(name= str print ,version=1.0.0 ,packages=find_packages(),include_package_data=True,description=str print ,author=xxx ,package_data={ strprint: [* .所以] },)编写明显的。

  ``文本/普通

  包含strprint/libstr_print.so

  打包发布。bashpython setup.py sdist上传Java接口对于爪哇接口,将其打包成冲突包,并发布至专家仓库中。

  编写封装接口代码JnaDemo.java。

  包com。jna。演示;导入com。星期日jna。图书馆;导入com。星期日jna。原生;导入com。星期日jna。指针;公立学校图书馆;公共接口图书馆扩展了库{指针创建();void str_print(字符串文本);}公共静态jna demo create(){ jna demo jna demo=new jna demo();jna演示。CLI brary=native。load( str _ print ,CLI brary。类);//系统。出去。println( test );返回jna demo } public void print(String text){ CLI library。str _ print(文本);} }创建资源目录,并将依赖的动态库放到该目录。

  通过打包插件,将依赖的库一并打包到冲突包中。

  插件工件id maven-汇编-插件/工件id配置追加程序集id false/追加程序集id描述符refs描述符ref jar-with-dependencies/描述符ref/描述符refs/配置执行id制造-装配/id阶段包/阶段目标目标组装/目标/目标/执行/执行/插件3.3业务使用3.3.1 Python使用安装strprint包。

  点安装strprint==1.0.0使用示例:

  # -*-编码:utf-8 -*-从strprint导入[计]系统复制命令(system的简写)导入* str_print(Hello py)3.3.2 Java使用砰的一声引入冲突包。

  依赖关系groupId com。jna。演示/组Id工件id jna演示/工件id版本1.0/版本/依赖关系使用示例:

  jna demo jna demo=new jna demo();jna演示。str _ print( hello jna );3.4 易用性优化3.4.1 Python版本兼容Python2与Python3版本的问题,是计算机编程语言开发用户一直诟病的槽点。因为工具面向不同的业务团队,我们没有办法强制要求使用统一的计算机编程语言版本,但是我们可以通过对工具库做一下简单处理,实现两个版本的兼容Python。com .版本兼容里,需要注意两方面的问题:

  语法兼容数据编码计算机编程语言代码的封装里,基本不牵扯语法兼容问题,我们的工作主要集中在数据编码问题上。由于Python 3的潜艇用热中子反应堆(海底热反应堆的缩写)类型使用的是采用双字节对字符进行编码编码,而在C中,我们需要的字符*是utf8编码,因此需要对于传入的字符串做utf8编码处理,对于C语言返回的字符串,做utf8转换成采用双字节对字符进行编码的解码处理。于是对于上例子,我们做了如下改造:

  # -*-编码:utf-8-*-import ctypes import OSI import sysdirname,_=OS。路径。分裂(OS。路径。abspath(_ _ file _ _))lib=ctypes。cdll。loadlibrary(dirname /libstr _ print。所以)lib。str _ print。arg types=[ctypes。c _ char _ p]lib。str _ print。restype=none def is _ python 3():返回sys。version _ info[0]==3 def encode _ str(输入):if _ is依赖管理在很多情况下,我们调用的动态库,会依赖其它动态库,比如当我们依赖的gcc/g版本与运行环境上的不一致时,时常会遇到找不到glibc_X.XX的问题,这时需要我们提供指定版本的libstdc.so与libstdc。

  为了实现开箱即用的目标,在依赖并不复杂的情况下,我们会将这些依赖也一并打包到发布包里,随工具包一起提供。对于这些间接依赖,在封装的代码里,并不需要显式的负载,因为计算机编程语言与爪哇的实现里,加载动态库,最终调用的都是系统函数dlopen。这个函数在加载目标动态库时,会自动的加载它的间接依赖。所以我们所需要做的,就只是将这些依赖放置到库函数能够查找到路径下。

  库函数查找依赖的顺序如下:

  从dlopen调用者ELF(可执行可链接格式)的DT_RPATH指定的目录看,ELF是so的文件格式,这里的DT_RPATH写在动态库文件中。常规手段下,我们无法修改这部分。在环境变量LD_LIBRARY_PATH指定的目录中查找,这是最常用的指定动态库路径的方法。从dlopen调用者ELF的DT_RUNPATH指定的目录看,也是so文件中指定的路径。从/etc/ld.so.cache中寻找,需要修改/etc/ld.so.conf文件构建的目标缓存。因为需要root权限,所以在实际生产中很少修改。从/lib中寻找系统目录,一般存储系统依赖的动态库。从/usr/lib找到并由root安装的动态库很少在生产中使用,因为它需要root权限。从上面的搜索序列可以看出,管理依赖关系的最佳方式是指定LD_LIBRARY_PATH变量,以包含我们的工具包中的动态库资源所在的路径。另外,对于java程序,我们还可以通过指定java.library.path的运行参数来指定动态库的位置,Java程序会将java.library.path与动态库文件名拼接起来,作为绝对路径传递给dlopen,其加载顺序在上述顺序之前。

  最后,在Java中还有一个细节需要注意。我们发布的工具包是以JAR包的形式提供的,本质上是一个压缩包。在Java程序中,我们可以通过Native.load()方法直接加载位于项目资源目录中的so。这些资源文件打包后,会放在JAR包的根目录下。

  但是dlopen无法加载这个目录。对于这个问题,最好的解决方案可以参考【2.1.3生成动态库】一节中的打包方法,将依赖的动态库组合成一个so,无需任何环境配置即可开箱即用。但是,对于libstdc .so.6等不能打包在so中的系统库,更常见的是在服务初始化时将so文件从JAR包复制到本地目录,并指定LD_LIBRARY_PATH包含该目录。

  4.原理介绍4.1为什么需要一个c_wrapper实现方案?Python/Java不能直接调用C接口,需要先把外部接口以C语言的形式封装在C中。这里的根本原因是在使用动态库中的接口之前,需要根据函数名找到接口在内存中的地址。动态库中函数的寻址由系统函数dlsym实现,严格按照传入函数名寻址。

  在C语言中,函数签名是代码函数的名字,但在C语言中,由于需要支持函数重载,可能会有几个函数同名。为了保证签名的唯一性,C通过名称管理机制为同名但实现不同的函数生成不同的签名。生成的签名会是类似_ __Z4funcPN4printE的字符串,dlsym无法识别(注:Linux系统中大多数可执行程序或动态库都是以ELF格式组织二进制数据,所有非静态函数都用“符号”唯一标识,在链接和执行时用来区分不同的函数,在执行时映射到特定的指令地址。这个“符号”通常称为函数签名)。

  要解决这个问题,我们需要用extern“C”来指定函数,用C的签名来编译,所以当依赖的动态库是C库时,需要用一个c_wrapper模块来桥接。但是当依赖库是用C语言编译的动态库时,就不需要这个模块,可以直接调用。

  4.2跨语言调用如何实现参数传递C/C函数调用的标准流程如下:

  在内存的堆栈空间中为被调用的函数分配一个堆栈帧,用来存储被调用函数的参数、局部变量和返回地址。将实参的值复制到相应的形参变量(可以是指针、引用或值副本)。控制流被移动到被调用函数的开始位置并被执行。控制流返回函数调用点,返回值给调用者,同时释放堆栈帧。从上面的过程可以看出,函数调用涉及到内存的申请和释放,实参到形参的复制等。Python/Java这种基于虚拟机的程序,其虚拟机内部也遵循上述流程,但是在调用非原生语言实现的动态库程序时,调用流程是什么样的?

  由于Python/Java的调用过程基本相同,我们就以Java的调用过程为例进行讲解,这里不再重复Python的调用过程。

  4.2.1内存管理在Java的世界里,内存是由JVM统一管理的。JVM的内存由栈区、堆区和方法区组成。在更详细的信息中,将提到本机堆和本机堆栈。实际上,我们从操作系统而不是JVM的角度来理解这个问题会更简单、更直观。以Linux为例。第一,JVM名义上是一个虚拟机,但本质是一个运行在操作系统上的进程,所以这个进程的内存会有如下左图所示的划分。但是JVM的内存管理本质上是重新划分进程堆,在Java世界里“虚拟”创建堆栈。如右图所示,native的堆栈区域就是JVM进程的堆栈区域。process的堆区域的一部分用于JVM管理,其余部分可以分配给本机方法。

  图3

  4.2.2调用过程如前所述,在调用一个原生方法之前,需要将其动态库加载到内存中。这个过程是利用Linux的dlopen实现的。JVM会把动态库中的代码片段放到本机代码区,同时会在JVM字节码区保存一个本机方法名和本机代码中内存地址之间的映射。

  调用一次本机方法有四个步骤:

  从JVM字节码中获取本机方法的地址。准备方法所需的参数。切换到本机堆栈并执行本机方法。在本机方法从堆栈中释放后,它切换回JVM方法,JVM将结果复制到JVM的堆栈或堆中。

  图4

  从上面的步骤可以看出,native方法的调用还涉及到参数的复制,复制是在JVM栈和native栈之间建立的。

  对于本机数据类型,参数通过值复制与本机方法地址堆叠在一起。对于复杂的数据类型,需要一套协议将Java中的对象映射到C/C中可识别的数据字节,原因是JVM和C语言中的内存排列差异较大,不能直接复制。这些差异主要包括:

  类型不一样。比如Java里char是16位,c里是8位,可能和JVM的字节顺序(Big Endian或者Little Endian)不一致。JVM的object会包含一些元信息,而C中的struct只是基本类型的并列。同样,Java中也没有指针,所以需要封装和映射。

  图5

  上图是native方法调用时参数传递的过程,其中映射复制是通过JNI的C/C link的glue代码实现的,类型的映射是在jni.h中定义的

  Java基本类型和C基本类型之间的映射(通过值传递。将JVM内存中Java对象的值复制到堆栈帧的参数位置):

  typedef无符号字符jbooleantypedef无符号短jchartypedef short jshorttypedef float jfloattypedef double jdoubletypedef jint jsizeJava复杂类型和C复杂类型之间的映射(通过指针传递。先根据基本类型,一一映射,把组装好的新对象的地址复制到栈帧的参数位置):

  typedef _ job object * job object;typedef _ jclass * jclasstypedef _ jthrowable * jthrowabletypedef _ jstring * jstringtypedef _ jarray * jarray注意:在Java中,所有非原生类型都是object的派生类,多个对象的数组也是一个对象,每个对象的类型都是一个类,类本身也是一个对象。

  class _ job object { };class _ jclass:public _ job object { };class _ jthrowable:public _ job object { };class _ jarray:public _ job object { };class _ jcharArray:public _ jarray { };class _ jobobjectarray:public _ jarray { };Jni.h提供了内存复制和读取的工具。比如上例中的GetStringUTFChars,可以将JVM中字符串中的文本内容按照utf8编码的格式复制到native heap中,并将char*指针传递给native方法使用。

  整个调用过程,生成的内存副本,Java中的对象都是由JVM的GC来清理的。如果原生堆中的对象是由JNI框架分配和生成的,比如上面JNI例子中的参数,它们都是由框架释放的。但是C/C中新分配的对象需要用户代码在C/C中手动释放,简而言之,Native Heap与普通C/C进程一致,没有GC机制,遵循谁分配谁释放的内存治理原则。

  4.3扩展阅读(JNA直接映射)与JNI相比,JNA使用其函数调用的基本框架,其中类型映射和内存复制的大部分工作由JNA工具库中的工具类自动完成,避免了大量胶水代码的编写,使用起来更加友好。但是,这部分工作有一些性能损失。

  JNA还提供了额外的“DirectMapping”调用方法来弥补这一不足。但直接映射对参数有严格限制,只能传递原生类型、对应数组和原生引用类型,不支持不定参数。方法返回类型只能是本机类型。

  native关键字需要添加到直接映射的Java代码中,这与JNI的写法一致。

  直接映射的示例

  导入com . sun . jna . *;public类jna demo { public static native double cos(DoubleByReference x);静态{ Native.register(平台。c _库_名);} public static void main(String[]args){ system . out . println(cos(new DoubleByReference(1.0)));}}DoubleByReference是双精度浮点数的本机引用类型的实现,其JNA源代码定义如下(仅截取相关代码):

  //DoubleByReferencepublic类DoubleByReference扩展了ByReference { public DoubleByReference(double value){ super(8);setValue(值);}}//ByReferencepublic抽象类ByReference扩展pointer type { protected by reference(int dataSize){ set pointer(new Memory(dataSize));} }内存类型是shared_ptr实现的Java版本,通过引用参数封装了内存分配、引用和释放的细节。这种类型的数据内存实际上是在本机堆中分配的。在Java代码中,只能获得对这个内存的引用。当JNA构造内存对象时,它通过调用malloc在堆中分配新的内存,并记录指向内存的指针。

  当释放ByReference对象时,调用free来释放内存。JNA源代码中的ByReference基类的finalize方法会在GC中被调用,释放对应应用的内存。所以在JNA的实现中,动态库中分配的内存由动态库的代码管理,JNA框架分配的内存由JNA的代码释放。然而,触发时间是JVM中的GC机制释放JNA对象的时候。这和前面提到的Native Heap没有GC机制,谁分配谁释放的原理是一致的。

  @ override protected void finalize(){ dispose();}/**释放本机内存,将peer设置为零*/protected synchronized void dispose(){ if(peer==0){//之前有人调用dispose,终结器会再次调用dispose return;}试试{免费(同行);}最后{ peer=0;//此处无null检查,跟踪仅对SharedMemory为null//shared memory正在重写dispose方法reference . unlink();}}4.4性能分析提高计算效率是原生调用中的一个重要目的,但是经过上面的分析我们不难发现,一个跨语言的本地化调用过程中仍然有大量的跨语言工作要做,这些过程也需要花费相应的计算能力。所以,并不是所有的原生调用都能提高计算效率。为此,我们需要了解语言之间的性能差异在哪里,跨语言调用需要多大的计算能力。

  语言之间的性能差异主要体现在三个方面:

  Python和Java都是解释性执行语言。运行时需要把脚本或者字节码翻译成二进制的机器指令,然后交给CPU执行。而C/C编译执行语言直接编译成机器指令执行。虽然有JIT等运行时优化机制,但也只能在一定程度上缩小这种差距。上层语言有很多操作,是由下层操作系统通过跨语言调用来实现的,效率明显不如直接调用。Python和Java语言的内存管理机制引入了垃圾收集机制,用来简化内存管理。GC工作时,会占用一些系统开销。这部分效率差异通常以跑步毛刺的形式出现,即对平均跑步时间没有明显影响,但对个别时刻的跑步效率影响较大。跨语言通话的费用主要包括三部分:

  对于JNA这种由动态代理实现的跨语言呼叫,在呼叫过程中存在栈交换、代理路由等任务。寻址和构造本地方法栈,即把Java中的原生方法映射到动态库中的函数地址,在调用站点上构造工作。内存映射,尤其是大量数据从JVM堆复制到Native堆时,这部分的开销是跨语言调用的主要耗时。我们通过下面的实验做了一个简单的性能比较。我们分别用C语言、Java、JNI、JNA、JNA直接映射计算了100万到1000万次余弦,得到了耗时的比较。在6核16G机器上,我们得到以下结果:

  图6

  图7

  根据实验数据,运行效率是C Java JNI JNA DirectMapping JNA。c比Java效率高,但两者非常接近。JNI和JNA DirectMapping的性能基本相同,但会比本地语言慢很多。正常模式下的JNA是最慢的,比JNI慢5到6倍。

  综上所述,跨语言本地化调用并不能总是提高计算性能,这就需要在计算任务的复杂度和跨语言调用的耗时之间进行综合平衡。目前我们总结了适合跨语言通话的场景如下:

  离线数据分析:离线任务可能涉及多种语言的开发,对耗时不敏感。最核心的一点是在多种语言中效果是扁平化的,跨语言调用可以节省多语言版本的开发成本。跨语言RPC调用转化为跨语言本地化调用:对于计算时间为微秒级或更短的计算请求,如果通过RPC调用获得结果,网络传输的时间至少为毫秒级,远大于计算开销。在简单依赖的情况下,通过将其转换为本地化调用,将大大减少单个请求的处理时间。对于一些复杂的模型计算,Python/Java跨语言调用C可以提高计算效率。5应用案例如上所述,通过本地化调用的解决方案可以在性能和开发成本上带来一些好处。我们在离线任务计算和实时服务调用中尝试了这些技术,取得了理想的效果。

  5.1将会有大量的离线计算任务,如词汇挖掘、数据处理、索引建立等。在离线任务的应用搜索业务中。这个过程会在查询理解中用到更多的文本处理和识别能力,比如分词、名名识别等。由于开发语言的差异,在本地重新开发这些功能是不可接受的。因此,在前面的任务中,在线服务将在离线计算的过程中被RPC调用。这种方案带来了以下问题:

  离线计算任务通常量级较大,执行过程中请求密集,会占用在线资源,影响在线用户的请求,安全性较低。单个RPC至少需要几毫秒,但实际计算时间往往很短,所以大部分时间其实都浪费在网络通信上,严重影响任务执行的效率。RPC服务因为网络抖动无法达到100%的成功率,影响任务执行效果。离线任务需要引入RPC调用相关代码。在Python脚本等轻量级计算任务中,由于一些基础组件的不完善,这部分代码往往会导致很高的访问成本。

  图8

  将RPC调用转化为跨语言本地化调用后,解决了上述问题,效益明显。

  不再调用在线服务,流量隔离,不影响在线安全。对于1000万以上的离线任务,累计节省网络开销时间至少10小时。消除网络抖动导致的请求失败问题。通过以上几章的工作,提供了开箱即用的本地化工具,大大简化了使用成本。

  图9

  5.2在线服务中的应用查询理解作为美团内部的基础服务平台,提供词性、查询纠错、查询重写、地标识别、异地识别、意图识别、实体识别、实体链接等文本分析。它是一个CPU密集型的大服务,在公司承担了很多本文分析的业务场景,有些只需要单个信号,甚至只需要查询理解服务中的基本功能组件。对于Java开发的大部分业务服务来说,不可能直接引用查询理解的C动态库。以前,结果一般是通过RPC调用获得的。通过以上工作,可以将非C语言调用者服务中的RPC调用转化为跨语言本地化调用,可以明显提高调用者的性能和成功率,同时有效降低服务器的资源开销。

  图10

  6总结微服务等技术的发展使得服务的创建、发布和访问变得更加容易。然而,在实际的工业生产中,并不是所有的场景都适合通过RPC服务进行计算。尤其是在计算密集型和时间敏感型的业务场景中,当性能成为瓶颈时,远程调用带来的网络开销成为业务无法承受之痛。本文对语言本地化调用技术进行了总结,并给出了一些实践经验,希望能为您解决类似问题提供一些帮助。

  当然,这部作品还有很多不足之处。比如因为实际生产环境的要求,我们的工作基本都集中在Linux系统上。如果是开放库的形式,让用户自由使用,可能就要考虑兼容Windows下的dll,Mac OS下的dylib等等。这篇文章可能还有其他缺点。请大家评论讨论。

  关于本文的源代码,请访问:Github

郑重声明:本文由网友发布,不代表盛行IT的观点,版权归原作者所有,仅为传播更多信息之目的,如有侵权请联系,我们将第一时间修改或删除,多谢。

相关文章阅读

  • windows下怎么安装perl,linux安装perl环境
  • windows下怎么安装perl,linux安装perl环境,Windows10下安装配置 perl 环境的详细教程
  • vue安装vuex,linux vue部署
  • vue 配置,vue linux
  • vue 配置,vue linux,Vue中的Vux配置指南
  • ubuntu unzip命令,Unzip命令,Linux命令unzip详解
  • rar文件怎么解压Linux,Linux rar文件怎么解压
  • rar文件怎么解压Linux,Linux rar文件怎么解压,Linux 下解压 rar 文件的方法
  • os.popen函数,popen用法,Linux中popen函数的作用小结
  • mkdir命令用法,命令行mkdir,Linux学习之mkdir命令详解
  • linux重启命令行,linux自动重启命令
  • linux重启命令行,linux自动重启命令,详解Linux 中五个重启命令
  • linux里chown命令,linux权限chown
  • linux里chown命令,linux中chown是什么意思,每天一个linux命令(30)- chown命令详解
  • linux里awk命令,linux常用命令awk
  • 留言与评论(共有 条评论)
       
    验证码: