1 Востаннє редагувалося Ярослав (18.01.2013 22:08:30)

Тема: Java Tip 141: Fast math with JNI

Use JNI to dramatically speed math in J2SE 1.4
By Jeff S. Smith, JavaWorld.com, 08/15/03

http://www.javaworld.com/javaworld/java … ip141.html
При розробці генерованої комп'ютером голограми (CGH), я помітив, що математичні процедури в Java 2 Platform, Standard Edition (J2SE) 1.4x виявились в кілька разів повільнішими, аніж відповідні процедури в J2SE 1.3.1. В Sun Microsystems розробили новий пакунок для математичних розрахунків, StrictMath, в J2SE 1.4, що гарантує ідентичні математичні обчислення для всіх платформ за рахунок швидкості виконання. Моя CGH програма працювала в сім разів повільніше з новими J2SE 1.4x процедурам! Я не маю бажання переходити на J2SE 1.3.1 знов, однак хочу вирішити проблему, для того щоб використовувати новий пакет javax.imageio, щоб мати змогу зберігати мої голограми на диск у форматі .png. Я надіслав декілька звітів про баги в Sun, але вони були проігноровані.

JNI, допоможи!

Коли я запримітив повільну роботу StrictMath, то звернув увагу на C++ компілятор і код, що ним використовується для проведення мат. обчислень. Мені просто потрібно було використати його функції C++ замість повільної StrictMath з програми Java. Java Native Interface (JNI) дозволяє взаємодіяти Java з машинним кодом, написаним на інших мовах, таких як C/C++. Це дає змогу писати левову частку вашої програми на Java і використовувати оптимізований машинний код, де це необхідно. І в корпоративному світі комп'ютерів, JNI дозволяє Java-додаткам підключатися до успадкованих (legacy) кодів, які важко або неможливо портувати на Java.

Щоб "побудувати" свою JNI математичну бібліотеку, я спочатку створив Java клас, MathLib, який оголошує три власних методи, cos(), sin() та sqrt(). Також я додав функції із стандартних бібліотек, щоб перевірити правильність виконання.

package com.softtechdesign.math;
/**
 * MathLib declares native math routines to get around the slow StrictMath math
 * routines in JDK 1.4x
 * @author Jeff S Smith
 */
public class MathLib
{
    public static native double sin(double radians);
    public static native double cos(double radians);
    public static native double sqrt(double value);
    static 
    {
        System.loadLibrary("MathLib");
    }
    
    public static void main(String[] args) 
    {
        System.out.println("MathLib benchmark..." + new java.util.Date());
        double d = 0.5;
        for (int i=0; i < 10000000; i++)
        {
            d = (d * i) / 3.1415926;
            d = MathLib.sin(d);
            d = MathLib.cos(d);
            d = MathLib.sqrt(d) * 3.1415926;
        }
        
        System.out.println("Benchmark done. Time is " + new java.util.Date());
    }
}

Статичний ініціалізатор, який виконується перед кодом в методі main, завантажує бібліотеки MathLib. Метод, що знаходиться в main виконує стандартний тест підпрограм, завдяки  їх виклику в середині циклу. Далі, я компілюю мій клас і створюю файл шапки (.h), для його подальшого використання стандартним java інструментом.

javah -jni -o MathLib.h -classpath . com.softtechdesign.math.MathLib

Ця команда створить файл під назвою Mathlib,h, в якому будуть прописані наступні функції cos(), sin()та sqrt(). Цей шапковий файл, написаний на мові C, містить <ini.h>, типовий шапковий файл для JNI застосунків. Зверніть увагу на те, як повністю пристосований клас просто стає макросом, написаним на C. MathLib.h виглядає так:

/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class com_softtechdesign_math_MathLib */
#ifndef _Included_com_softtechdesign_math_MathLib
#define _Included_com_softtechdesign_math_MathLib
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     com_softtechdesign_math_MathLib
 * Method:    sin
 */
JNIEXPORT jdouble JNICALL Java_com_softtechdesign_math_MathLib_sin
  (JNIEnv *, jclass, jdouble);
/*
 * Class:     com_softtechdesign_math_MathLib
 * Method:    cos
 */
JNIEXPORT jdouble JNICALL Java_com_softtechdesign_math_MathLib_cos
  (JNIEnv *, jclass, jdouble);
/*
 * Class:     com_softtechdesign_math_MathLib
 * Method:    sqrt
 */
JNIEXPORT jdouble JNICALL Java_com_softtechdesign_math_MathLib_sqrt
  (JNIEnv *, jclass, jdouble);
#ifdef __cplusplus
}
#endif
#endif

Наступним кроком я створюю файл Math.c і вставляю сирцевий код на C, який містить три математичні підпрограми, що використовують підписи, розміщені в Mathlib.h. Увага! Ці - стандлартні C функціями sin(), cos() та sqrt()

#include <jni.h>
#include <math.h>
#include "MathLib.h"
#include <stdio.h>
JNIEXPORT jdouble JNICALL Java_com_softtechdesign_math_MathLib_sin(JNIEnv *env, jobject obj, jdouble value)
{
    return(sin(value));
}
JNIEXPORT jdouble JNICALL Java_com_softtechdesign_math_MathLib_cos(JNIEnv *env, jobject obj, jdouble value)
{
    return(cos(value));
}
JNIEXPORT jdouble JNICALL Java_com_softtechdesign_math_MathLib_sqrt(JNIEnv *env, jobject obj, jdouble value)
{
    return(sqrt(value));
}

Всі підпрограми повертають jdouble і мають два стандартні JNI параметри, JNIEnv (JNI покажчик на інтерфейс) і jobject (посилання на виклик об'єктом самого себе, схожий на this). Всі реалізовані тут JNI підпрограми мають ці два параметри (навіть якщо власний метод не оголошує будь-які інші параметри). Третій параметр є моїм (визначеним користувачем) параметром, який містить значення, яке в свою чергу передається в функцію. Ці процедури розраховують sin(), cos() чи sqrt() цього параметра і повертає значення, як jdouble.

Кілька слів про типи JNI

Сирцеві методи мають доступ до наступних типів

Boolean  jboolean
string   jstring
byte     jbyte
char     jchar
short    jshort
int      jint
long     jlong
float    jfloat
double   jdouble
void     void 

Всі ці типи можуть бути доступні безпосередньо, за винятком jstring, який вимагає виклику підпрограм для перетворення Java Unicode рядків (2 байти) на Сі-шні покажчики типу char на рядки (1 байт UTF-8 форматі). Для того, щоб реалізувати таке перетворення, необхідно застосувати наступний код:

JNIEXPORT void JNICALL
Java_MyJavaClass_printName(JNIEnv *env, jobject obj, jstring name)
{
    const char *str = (*env)->GetStringUTFChars(env, name, 0);
    printf("%s", name);
      //Need to release this string when done with it
      //to avoid memory leak
    (*env)->ReleaseStringUTFChars(env, name, str);
}

Ви можете використовувати метод (* ENV) -> NewStringUTF (), щоб створити новий jstring із рядка в стилі Сі. Наприклад, функція C може містити наступний код:

JNIEXPORT jstring JNICALL
Java_MyJavaClass_getName(JNIEnv *env, jobject obj)
{
    return (*env)->NewStringUTF(env, "Fred Flintstone");
}

Повертаючись до обробки MathLib

Продовжуючи писати MathLib, я компілюю мій C код в бібліотеку .dll ((Dynamic Link Library), так як використовую Windows). Працюю в Borland C++ Builder для її створення. Якщо ви використовуєте С++ Builder, переконайтеся, що ви створюєте .DLL проект (під назвою MathLib), а потім прикріпіть Math.c файл до проекту. Також треба додати іще наступний код в include.

  \javadir\include
  \javadir\include\win32

C++ будівничий :) створює бібліотеку під назвою MathLib.dll в процесі побудови проекту.
Якщо ви компілюєте Windows C бібліотеку на Visual C++, то вам знадобиться наступна команда:

cl -Ic:\javadir\include -Ic:\javadir\include\win32 -LD Math.c -FeMathLib.dll

Під Solaris - ця

cc -G -I/javadir/include -I/javadir/include/solaris Math.c -o MathLib.so

Останнім кроком буде додавання могої .DLL каталогу до каталогу runtime library (як альтернатива, я можу скопіювати DLL в робочий каталог моєї Java програми). Якщо код Java не може знайти бібліотеку, ви отримаєте java.lang.UnsatisfiedLinkError. Спробуйте запустити Java-програму:

java -classpath . com.softtechdesign.math.MathLib

Перевірте, в який спосіб працює стандартний код з методу main. Якщо ви хочете порівняти швидкість виконання операцій в цих версіях StrictMath, то ви маєте змінити спосіб виклику в MathLib.java з MathLib.xxx (), на Math.xxx (). На моєму Pentium 4, 2-ГГц StrictMath процедра відбувається на протязі 24.5 с, а MathLib (JNI) - 7 с. Не поганий приріст продуктивності, чи не атак!

MathLib (JNI) процедури приблизно в два рази швидше, ніж Math процедури в J2SE 1.3.1 - це спричинено  додатковими витратами продуктивності на виклик JNI. Зверніть увагу, що коли ви робите численні заклики до MathLib підпрограм в циклі, ви іноді отримуєте виняток виду:

Unexpected Signal : EXCEPTION_FLT_STACK_CHECK occurred at PC=0x9ED0D2

Я надіслав звіт щодо цього багу до Sun, але не отримав відповіді. Проте гарною новиною є те, що на практиці, я ніколи не отримував помилку при використанні даного програмного коду для голограм.

JNI дозволяє програмістам використовувати швидкі C/C++ процедури для математичних обчислень в своїх програмах Java. Він вирішує завдання, для яких Java погано підходить: коли ви потребуєте швидкості С або асемблера, або коли вам потрібно написати низькорівневий код, щоб безпосередньо керувати обладнанням. Вона також стане у пригоді для використання з бібліотеками, написаними на інших мовах.

Цей "MathLib" приклад тільки поверхнево продемонстрував те, що б ви могли виробляти з JNI. JNI вбудовані методи можуть створювати і маніпулювати Java-об'єктами: рядками, масивами, а також опрацьовувати Java-об'єкти як параметри. JNI може навіть опрацьовувати код Java відповідно до власних вбудованих виключень. Це це дозволяє дуже легко впровадджувати власні методи в додатках Java.
Про автора
Джефф С. Сміт - це президент SoftTech Design, що розробляє ПЗ і Web-ресурси та розташована неподалік від Денвера, штат Колорадо. Джефф має ступінь бакалавра в галузі фізики в університеті Вірджинії і 15 років стажу розробки програм / баз даних / веб-проектів, а також 6 років досвіду роботи в якості Delphi і Java інструктора. Він є автором численних ігор та програм для домашніх розваг і автор статей про генетичні алгоритми і Java Database Connectivity (JDBC) фреймворки. Більше його статей тут: http://www.SoftTechDesign.com/media.html
Переклад: keithfay