注:本文亦适用borland c++ builder 5.0
?前言
笔者最近从事一个利用java来发展密码模块的工作,由于利用纯java语言所制作出来的密码模块效率实在不好,最后我们把脑筋动到jni(java native interface)上. 为何会想到使用jni呢? 大家应该都知道java程序的执行必须透过java virtual machine,透过一层中介的结果,执行的效率必然比c/c++所编译出来的原生码(native code,即专属各处理器的指令集)还要慢. 事实上,在jdk内附的java.math 这个package里头,许多部分也都应用了jni来加快运算速度(例如big integer运算).
?硬件优势
一旦利用的jni,代表我们将能够连结c/c++或是assembly所撰写的加密模块. 为了加快密码模块的performance,必须运用一些硬件上的优势,例如说当处理区块加密运算时,如果能运用一点并行处理的观念,就能够适当地加快运算速度. 以目前的pc上的处理器来说,支持平行处理能力的技术就属大家所熟知的mmx, streaming simd extension(大家也许比较熟悉的是kni这个名词),以及3dnow!. 这些技术其实就是实做了simd(single-instruction, multiple-data)的概念,允许处理器在同一时间之内,使用单一指令,就可以同时处理好几组数据.
另外,在pentium等级以上的cpu具有利用pipeline来加快执行速度的能力,只要调整assembly code的排列顺序,使其符合intel scheduling rules,就可以充分利用cpu里头的u-pipe与v-pipe,加快执行速度. 其实,就笔者所使用的visual c++ 6.0与borland c++ builder 4.0来说,虽然都有编译器指令可以针对处理器做最佳化,但是如果您亲自去看看编译出来的结果,能然有很多地方无法尽如人意,因此如果遇到time critical的部分,仍然常常需要我们亲自去调校以改善performance.
?准备工作及注意事项
ok,讲到这边,似乎离主题有点远了,让我们回归正题吧!
为了让我们可以在执行时期动态地依照cpu的能力来执行最佳化的程序代码,首要的工作就是要写一些函式来侦测cpu的特性,于是笔者选择了jdk 1.2以及bcb 4.0来完成整个由
java code è jni è platform native code
的完整测试程序.
如果以图片来表示就如下图
如果您抓取了笔者提供的原始码,应该可以看到下面三个分别由java与c++撰写的程序模块:
cputestdll.bpr cputestdll.cpp
实做侦测cpu特性相关函式的模块.
此为bcb 4.0之 项目文件,使用project/make cputest的指令后,所产生的结果cputest.dll,是我们所要的.
编译注意事项 :
由于在cputestdll.cpp里头我们用到汇编语言指令cpuid,所以请打开project/option里头的advanced compiler次页,里头有一个叫instruction set的地方,请勾选pentium,否则编译器会因为不支持此指令而产生编译错误.如果您要把编译过的结果给别人使用,建议您将project/option/package次页中的build with run-time package选项 以及 linker次页中的use dynamic rtl选项通通取消掉.
请打开project/option/directories conditionals次页,将jdk所在目录include
与jdk所在目录includewin32加到include path里头 ; 另外也在library path中加入 jdk所在目录lib,否则会造成编译错误.
cputest.java
这个java程序是作为其它java程序透过jni以呼叫cputestdll.dll的接口. 笔者把这个接口宣告于my.cpu这个package底下.
编译注意事项 :
编译java程序时,请设定环境变量path与classpath
例如jdk安装在c:jdk1.2这个目录,而此档案放在c:jdk1.2my之下,
那幺请在提示符号下命令
path c:jdk1.2in
set classpath=c:jdk1.2classes;c:jdk1.2my
test.java
这个java程序将利用cputest对象当作接口,来呼叫实做于cputestdll.dll内的cpu特征侦测函数
编译注意事项 :
除了2的注意事项外,请将cputest.java放到 <jdk安装目录>classesmycpu这个目录之中,否则编译将无法通过.
?参考文件
1. jdk 1.2 on-line document
2. intel architecture optimization/reference manual order number: 245127-001
3. amd 3dnow! technology manual
?用jdk 实做jni接口
首先,为了让所有的java code都可以使用我们的cpu特征侦测函数,我们首先必须先制作一个接口类别:
档案列表cputest.java
/*********************************************************************************
cputest.java
jni 接口对象
1999 april 20 by 王森
**********************************************************************************/
//加入my.cpu这个package之中
package my.cpu ;
public class cputest {
/*以下定义每种处理器所代表的常数*/
static public final int i386 = 0 ; //不支持cpuid的处理器(可辨识)
static public final int pentium = 1 ; //最早期的pentium处理器(可辨识)
static public final int pentium_m = 2 ; //pentium with mmx 处理器(可辨识)
static public final int pentium_2 = 3 ; //pentium ii 处理器(可辨识)
static public final int pentium_3 = 4 ; //pentium iii处理器(可辨识)
static public final int pentium_p = 5 ; //pentium pro 处理器(可辨识)
static public final int k6 = 11 ; //同pentium with mmx
static public final int k6_2 = 12 ; //k6-2处理器((可辨识)
static public final int k6_3 = 13 ; //同k6-2
/*以下定义所有会藉由jni来叫用的函式*/
//测试cpu是否支持cpuid指令,如果支持则传回true,否则传回false
public native boolean checkcpuid() ;
^^^^^^ 注意,所有的jni函式都必须在函式宣告里加上native这个修饰字
//辨识处理器是否支持mmx,如果支持则传回true,否则传回false
public native boolean checkmmx() ;
//辨识处理器是否支持stream simd extension(即kni),如果支持则传回true,否则传回false
public native boolean checkssimd() ;
//辨识处理器是否支持amd 3dnow,如果支持则传回true,否则传回false
public native boolean check3dnow() ;
//辨识cpu的等级,并传回一个整数代表cpu的等级
public native int checkcputype() ;
//印出cpu的相关信息
public native void printcpuinfo() ;
note:使用此函数之前,请先呼叫前面的所有函式,因为前面的函式,除了传回真伪之外,也会设定dll文件之中的全域变量而printcpuinfo会利用这些全域变量来做判定的工作.
static {
我们把实做cpu侦测函式的模块做成dll(动态连结函式库)?,
取名叫cpudtestdll.dll,所以在这里要加载此dll
system.loadlibrary("cputestdll") ;
}
}
接着我们在提示符号下使用指令
javac cputest.java
编译此档案,会产生cputest.class这个档案.然后我们把这两个档案都移至
<jdk安装目录>/classes/my/cpu/
这个目录底下,如果没有做此动作,恐怕下面的步骤都会遇到一些错误.
最后一个步骤,就是必须产生一个引入档(include file),我们将会在编译cputestdll.dll实用到这个引入档.
在提示符号下使用指令
javah my.cpu.cputest
就会在您目前的工作目录下看到
my_cpu_cputest.h
到此为止,我们已经完成了第一个阶段.
案列表my_cpu_cputest.h
/* do not edit this file - it is machine generated */
#include <jni.h>
/* header for class my_cpu_cputest */
#ifndef _included_my_cpu_cputest
#define _included_my_cpu_cputest
#ifdef __cplusplus
extern "c" {
#endif
#undef my_cpu_cputest_i386
#define my_cpu_cputest_i386 0l
#undef my_cpu_cputest_pentium
#define my_cpu_cputest_pentium 1l
#undef my_cpu_cputest_pentium_m
#define my_cpu_cputest_pentium_m 2l
#undef my_cpu_cputest_pentium_2
#define my_cpu_cputest_pentium_2 3l
#undef my_cpu_cputest_pentium_3
#define my_cpu_cputest_pentium_3 4l
#undef my_cpu_cputest_pentium_p
#define my_cpu_cputest_pentium_p 5l
#undef my_cpu_cputest_k6
#define my_cpu_cputest_k6 11l
#undef my_cpu_cputest_k6_2
#define my_cpu_cputest_k6_2 12l
#undef my_cpu_cputest_k6_3
#define my_cpu_cputest_k6_3 13l
/*
* class: my_cpu_cputest
* method: check3dnow
* signature: ()z
*/
jniexport jboolean jnicall java_my_cpu_cputest_check3dnow
(jnienv *, jobject);
/*
* class: my_cpu_cputest
* method: checkcpuid
* signature: ()z
*/
jniexport jboolean jnicall java_my_cpu_cputest_checkcpuid
(jnienv *, jobject);
/*
* class: my_cpu_cputest
* method: checkcputype
* signature: ()i
*/
jniexport jint jnicall java_my_cpu_cputest_checkcputype
(jnienv *, jobject);
/*
* class: my_cpu_cputest
* method: checkmmx
* signature: ()z
*/
jniexport jboolean jnicall java_my_cpu_cputest_checkmmx
(jnienv *, jobject);
/*
* class: my_cpu_cputest
* method: checkssimd
* signature: ()z
*/
jniexport jboolean jnicall java_my_cpu_cputest_checkssimd
(jnienv *, jobject);
/*
* class: my_cpu_cputest
* method: printcpuinfo
* signature: ()v
*/
jniexport void jnicall java_my_cpu_cputest_printcpuinfo
(jnienv *, jobject);
#ifdef __cplusplus
}
#endif
#endif
附带一提,上面这个档案完全是使用javah这个jdk内附的程序所产生,我们不要去修改它,以免发生更多意想不到的麻烦.
?c++ builder 4.0实做native端
好,完成了接口部分,接下来就要开始实做部分啦! 首先,请先打开您的bcb 4.0,选择file/new里头的new次页中,选择开启一个dll项目档. 开启成功后,请将此项目的档名取名为cputestdll. 您就会在目录下看到cputestdll.bpr与cputestdll.cpp两个档案,另外,为了让编译工作顺利,您必须再调校一些编译器选项,请参照前面 ?准备工作及注意事项.
首先,请在cputestdll.cpp里头将my_cpu_cputest.h这个档案引入
#include "my_cpu_cputest.h"
否则会产生”undefined symbol xxxx”的错误
完整程序代码如下:
档案列表cputestdll.cpp
#include <vcl.h>
#pragma hdrstop
#include <iostream.h>
#include "my_cpu_cputest.h"
/*底下定义一些代表cpu的变量*/
const int i386 = 0 ; //不支持cpuid的处理器(可辨识)
const int pentium = 1 ; //最早期的pentium处理器(可辨识)
const int pentium_m = 2 ; //pentium with mmx 处理器(可辨识)
const int pentium_2 = 3 ; //pentium ii 处理器(可辨识)
const int pentium_3 = 4 ; //pentium iii处理器(可辨识)
const int pentium_p = 5 ; //pentium pro 处理器(可辨识)
const int k6 = 11 ; //同pentium with mmx
const int k6_2 = 12 ; //k6-2处理器((可辨识)
const int k6_3 = 13 ; //同k6-2
/*****************************************************/
/*以下定义一些辨识cpu能力的变量*/
bool cpuid_s = false ; //测试是否支持cpuid指令
bool mmx = false ; //测试是否支持mmx
bool ssimd = false ; //测试是否支持streaming simd extension
bool _3dnow = false ; //测试是否支持3d!now
int cputype = i386 ; //cpu的型态,初始值为i386
/*****************************************************/
下面我们将开始介绍这些原生函式的实做方式,但是有些观念我们必须要先知道. 首先,在下面的函式里头,我们利用bcb内嵌汇编语言来实做,虽然我们也可以利用win32 api或其它方法来取得系统信息和硬件信息,可是就复杂度来说,实在不如用汇编语言来的那幺简洁.(网络上常有人在争论汇编语言跟高级语言的优劣,甚至认为汇编语言已经没有存在的必要,不过笔者还是觉得做什幺事就用最适合的语言会比较好).
第二,在这些inline assembly code里面,我们大量地利用了cpuid这个汇编语言指令.这是一个辨识cpu相当好用的指令,除了辨识一些cpu的特殊能力,也可以提供一些厂方信息.大家可以翻阅intel的instruction set reference来看看这个指令的用法.不过呢,这个指令只有在pentium等级cpu中才提供,换句话说,486,386上这个指令应该无效,严格地说来,即使执行文件在486以下的计算机执行这段程序,应该是没有问题才对. 可是大家应该还记得去年在intel的cpu中一些don’t care的指令集竟然会造成计算机当机的错误吧! 所以我想在使用这个指令以前,应该先看看cpu是否支持这个指令,如果不支持,就不要再做下去,以免发生不可预期的错误. 因此我们在使用checkmmx, checkssimd, check3dnow,这些函式以前,请务必先执行checkcpuid这个函式.这个函式会去变更全域变量cpuid_s,因此不论是checkmmx, checkssimd, check3dnow,都会在使用cpuid指令前先检查这个全域变量,如果是false,就不再继续动作下去,以免发生非预期的错误.
最后一点,就是当c++ builder在编译内嵌组和语言的程序代码时,会在程序目录中产生cputestdll.asm这个中间档,请将project/option里头的advanced compiler次页,里头有一个叫instruction set的地方,请勾选pentium,否则不管您勾选386或是486,编译器会因为不支持此指令而产生组译错误.
/*以下开始实做所有的jni函式*/
/*辨识处理器是否支持amd 3dnow*/
jniexport jboolean jnicall java_my_cpu_cputest_check3dnow(jnienv *j, jobject o)
{
//如果不支持cpuid指令,就不必再做下去以免发生错误
if( cpuid_s == false )
return false;
unsigned long temp ;
asm mov eax,80000001h ;
asm cpuid ;
asm mov temp,edx ;
//第31个bit为3d!now的特征值
if ( temp & 0x80000000 )
{
_3dnow = true ;
return true ;
}
return false;
}
/*测试cpu是否支持cpuid指令*/
jniexport jboolean jnicall java_my_cpu_cputest_checkcpuid(jnienv *j, jobject o)
{
//以下程序用来测试cpu是否支持cpuid指令
unsigned int a,b ;
asm pushfd ;
asm pop eax ;
asm mov ebx,eax ;
asm xor eax,00200000h ;
asm push eax ;
asm popfd ;
asm pushfd ;
asm pop eax ;
asm mov a,eax ;
asm mov b,ebx ;
if ( a != b )
{
cpuid_s = true ;
return true ;
}
return false ;
}
/*辨识cpu的等级*/
jniexport jint jnicall java_my_cpu_cputest_checkcputype(jnienv *j, jobject o)
{
//如果不支持cpuid指令,就不必再做下去以免发生错误
if( cpuid_s == false )
return i386 ;
unsigned int temp ;
asm mov eax,0 ;
asm cpuid ;
asm mov temp,eax ;
if (temp == 2)//这是p6家族的情形
{
cputype = pentium_p ; //p6家族的第一颗processor为pentium pro
if (ssimd) //p6然后又支持ssimd ..一定是pentium iii
{
cputype = pentium_3 ;
return pentium_3 ;
}
if (mmx) //否则p6然后又支持mmx ..一定是pentium ii
{
cputype = pentium_2 ;
if( _3dnow )//支持mmx又支持3d!now,一定是k6-2
{
cputype = k6_2 ;
return k6_2 ;
}
return pentium_2 ;
}
//如果都没有支持以上这些多媒体指令集,那幺应该是pentium pro了
return pentium_p ;
}
if (temp == 1)//这是p5家族的情形
{
cputype = pentium ; //p5家族的第一颗processor为pentium
if (mmx) //p5然后又支持mmx ..一定是pentium with mmx
{
cputype = pentium_m ;
return pentium_m ;
}
return pentium ;
}
return i386 ;
}
/*辨识处理器是否支持mmx*/
jniexport jboolean jnicall java_my_cpu_cputest_checkmmx(jnienv *j, jobject o)
{
//如果不支持cpuid指令,就不必再做下去以免发生错误
if( cpuid_s == false )
return false ;
unsigned long temp ;
asm mov eax,1 ;
asm cpuid ;
asm mov temp,edx ;
//第23个bit为mmx的特征值
//p.s bit的编号由0 ~ 31
if ( temp & 0x00800000 )
{
mmx = true ;
return true ;
}
return false ;
}
/*辨识处理器是否支持stream simd extension(即kni)*/
jniexport jboolean jnicall java_my_cpu_cputest_checkssimd(jnienv *j, jobject o)
{
//如果不支持cpuid指令,就不必再做下去以免发生错误
if( cpuid_s == false )
return false ;
unsigned long temp ;
asm mov eax,1 ;
asm cpuid ;
asm mov temp,edx ;
//第25个bit为streaming simd extension的特征位
//p.s bit的编号由0 ~ 31
if ( temp & 0x02000000 )
{
ssimd = true ;
return true ;
}
return false ;
}
/*印出cpu的相关信息*/
jniexport void jnicall java_my_cpu_cputest_printcpuinfo(jnienv *j, jobject o)
{
cout << "... verify some processor information ..." << endl ;
cout << "the capacity of your processor : " << endl ;
if ( mmx )
{
cout << "support intel mmx technology" << endl ;
}else
{
cout << "no intel mmx technology support" << endl ;
}
if ( ssimd )
{
cout << "support intel streaming simd extensions" << endl ;
}else
{
cout << "no intel streaming simd extensions support" << endl ;
}
if ( _3dnow )
{
cout << "support amd 3d!now technology" << endl ;
}else
{
cout << "no amd 3d!now technology support" << endl ;
}
cout << "cpu type :" ;
switch( cputype )
{
case i386 :
cout << "general i386 processor" << endl ;
break ;
case pentium :
cout << "intel pentium processor" << endl ;
break ;
case pentium_m :
cout << "intel pentium with mmx processor" << endl ;
break ;
case pentium_2 :
cout << "intel pentium ii processor" << endl ;
break ;
case pentium_3 :
cout << "intel pentium iii processor" << endl ;
break ;
case pentium_p :
cout << "intel pentium pro processor" << endl ;
break ;
case k6 :
cout << "amd k6 processor" << endl ;
break ;
case k6_2 :
cout << "amd k6 ii processor" << endl ;
break ;
case k6_3 :
cout << "amd k6 iii processor" << endl ;
break ;
}
cout << "... verify end ..." << endl ;
}
int winapi dllentrypoint(hinstance hinst, unsigned long reason, void*)
{
return 1;
}
//---------------------------------------------------------------------------
大家可能 会发现每个函式在回传布尔值以前,都会先去设定一个dll内部的全域变量,这是因为效率的考量,假如读者有兴趣扩充笔者的程序,让java程序可以因为cpu的特征去呼叫适当的函式,一旦你遇到两次这种情形,您就必须呼叫checkxxxx函式两次,十次这种情形,您就要呼叫十次,相当地没有效率,如果照笔者的实做法,只要呼叫一次,就会去设定纪录cpu特征的全域变量,接下来就不必再次呼叫checkxxxx,直接存取全域变量就可以了. 大家可以参考一下printcpuinfo函式,大致上可以知道为何笔者这样做的理由.
还有就是checkcputype这个函式的方法并非是最正规的方法,笔者只是假设市面上只有amd与intel两家厂商而已,如果要正确的辨认出厂商跟型号,我们还必须利用cpuid取得更多信息才行,这就当作给诸位读者一个练习的机会,相信只要读者有心,去找找各种cpu的system programming manual,一定可以找到正确的侦测方法.
?测试
终于到了要测试咱们程序正确性的时候了,所以我们撰写了test.java
档案列表test.java
import my.cpu.* ;
class test
{
/*以下定义每种处理器所代表的常数*/
static public final int i386 = 0 ; //不支持cpuid的处理器(可辨识)
static public final int pentium = 1 ; //最早期的pentium处理器(可辨识)
static public final int pentium_m = 2 ; //pentium with mmx 处理器(可辨识)
static public final int pentium_2 = 3 ; //pentium ii 处理器(可辨识)
static public final int pentium_3 = 4 ; //pentium iii处理器(可辨识)
static public final int pentium_p = 5 ; //pentium pro 处理器(可辨识)
static public final int k6 = 11 ; //同pentium with mmx
static public final int k6_2 = 12 ; //k6-2处理器((可辨识)
static public final int k6_3 = 13 ; //同k6-2
//主程序开始
public static void main(string args[])
{
boolean temp ;
//取得jni接口对象
cputest my = new cputest() ;
//开始cpu相关信息的初始化工作
temp = my.checkcpuid() ;
if( temp )
{
system.out.println("cpuid support") ;
}else
{
system.out.println("cpuid not support") ;
}
temp = my.checkmmx() ;
if( temp )
{
system.out.println("mmx support") ;
}else
{
system.out.println("mmx not support") ;
}
temp = my.checkssimd() ;
if( temp )
{
system.out.println("ssimd support") ;
}else
{
system.out.println("ssimd not support") ;
}
temp = my.check3dnow() ;
if( temp )
{
system.out.println("3dnow support") ;
}else
{
system.out.println("3dnow not support") ;
}
system.out.println("") ;
system.out.println("---------starting java code print--------") ;
switch(my.checkcputype())
{
case i386:
system.out.println("i386") ;
break ;
case pentium:
system.out.println("pentium") ;
break ;
case pentium_m:
system.out.println("pentium with mmx") ;
break ;
case pentium_2:
system.out.println("pentium ii") ;
break ;
case pentium_3:
system.out.println("pentium iii") ;
break ;
case pentium_p:
system.out.println("pentium pro") ;
break ;
case k6:
system.out.println("k6") ;
break ;
case k6_2:
system.out.println("k6-2") ;
break ;
case k6_3:
system.out.println("k6-3") ;
break ;
}
system.out.println("") ;
system.out.println("---------starting native code print--------") ;
my.printcpuinfo() ;
}//end of main
}//end of class
在命令列下打入javac test.java,就可以产生test.class这个档案.接者请打java test来执行程序,不过首先您会先遇到下面的错误讯息:
c:jdk1.2my>java test
exception in thread "main" java.lang.unsatisfiedlinkerror: no cputestdll in java
.library.path
at java.lang.classloader.loadlibrary(compiled code)
at java.lang.runtime.loadlibrary0(runtime.java:470)
at java.lang.system.loadlibrary(system.java:745)
at my.cpu.cputest.<clinit>(cputest.java:48)
这是什幺原因呢? 原来是因为java virtual machine找不到cputestdll.dll,所以产生了执行时期例外.解决这个问题的方法有两种: 第一种就是把cputestdll.dll拷贝到跟test.class同一个目录下. 第二种方法就是下指令java -djava.library.path=<dll所在位置> test,例如:
java -djava.library.path=c:jdk1.2mydll test代表cputestdll.dll是放置在c:jdk1.2mydll底下. 不论您用哪种方法,都可以看到下面的输出结果:
c:jdk1.2my>java test
cpuid support
mmx support
ssimd not support
3dnow not support
---------starting java code print--------
pentium ii
---------starting native code print--------
... verify some processor information ...
the capacity of your processor :
support intel mmx technology
no intel streaming simd extensions support
no amd 3d!now technology support
cpu type :intel pentium ii processor
... verify end ...
以上是在笔者计算机上的执行情形.您的计算机上也成功地输出结果了吗?
?结语
其实在本文章中,只应用了jni帮我们传递一些参数给动态连结函式库,再由动态连结函式库传回一些值给我们而已. 其实,利用jni也可以做到直接更改动态连结函式库里头的变量,动态连结函式库里头的函式也可以直接更改java程序中的变量,只不过本文章重点并非在jni功能的介绍,所以并没有提及. 另外关于cpu特征的侦测,我们只是简单地侦测几个cpu的特性,较复杂的还有侦测cpu的时脉等特征,在intel的网站上我们也可以找到名为cpuinfo.zip的样本. 期望这篇文章可以让目前正在使用java,又想充分利用处理器特性来做最佳化的朋友提供一个踏板.
闽公网安备 35060202000074号