Biuletyn nr 23

Biuletyn KDM
1 | 2 | 3 | 4 | 5
6 | 7 | 8 | 9 | 10
11 | 12 | 13 | 14
15 | 16 | 17 | 18
19 | 20 | 21 | 22
23 | 24 | 25 | 26
27 | 28 | 29 | 30
31 | 32
Lista biuletynów

Biuletyn nr 23 (4 sierpnia 2007)

Spis treści

Java Native Interface (JNI) cz.1

Autor: Batosz Borucki

Stając przed zadaniem napisania aplikacji, programista znający kilka języków programowania zawsze napotyka problem wyboru odpowiedniego języka. Ponieważ jednak każdy z języków ma swoje charakterystyczne mocne i słabe strony, decyzja musi w bardzo dużym stopniu zależeć od konkretnej aplikacji. Jeżeli weźmiemy pod uwagę dwa, najpopularniejsze chyba, języki programowania C/C++ i Javę, możemy długo i bezowocnie dyskutować nad wyższością jednego nad drugim. W skrócie możemy jednak na pewno stwierdzić, że Java niosąc ze sobą łatwość programowania, wygodę budowania interfejsów graficznych, elastyczność i niezależność platformową, jest niestety mało wydajna obliczeniowo i nie daje programiście tak dużych możliwości optymalizacji, jak C/C++. Z drugiej strony C/C++ mając silną pozycję w świecie obliczeniowym, jest językiem szybkim i wydajnym, dając jednocześnie programiście większą kontrolę (np. nad pamięcią). Jednocześnie, ma on niestety bardziej skomplikowany i wymagający więcej pracy kod.

W dwóch kolejnych numerach biuletynu postaram się przybliżyć nieco temat, który w pewnym stopniu jest rozwiązaniem tego sporu - JNI czyli Java Native Interface. Jest to interfejs programistyczny umożliwiający osadzanie kodu natywnego, napisanego w C/C++, w aplikacjach Javy, i odwrotnie - osadzanie kodu Javy w aplikacjach natywnych napisanych w C/C++.


Niezbędne narzędzia

Do skorzystania z możliwości programowania łączącego C/C++ z Javą konieczne są następujące składniki:

  • środowisko Java Development Kit (zalecana wersja >=1.2) zawierające:
    • kompilator Javy - javac
    • wirtualną maszynę Javy (JVM) - java
    • generator metod natywnych - javah
    • biblioteki i pliki nagłówków JNI: jni.h, jvm.lib, jvm.dll/jvm.so
  • dowolny kompilator C/C++

Wszystkie elementy wymagane od strony Javy dostarczane są w podstawowym środowisku JDK.


Wywoływanie kodu C/C++ z programów Java

Procedurę stworzenia programu napisanego w Javie i wykorzystującego natywne metody napisane w C/C++ można podzielić na 5 etapów:

  • napisanie kodu w Javie
  • skompilowanie programu w Javie
  • utworzenie pliku z nagłówkami C/C++
  • napisanie kodu w C/C++
  • utworzenie biblioteki współdzielonej z napisanego kodu C/C++


Kod w Javie

Postępując zatem zgodnie z powyższym planem zaczynamy od napisania kodu w Javie. Utworzymy program, który będzie korzystał z czterech metod natywnych:

  • metoda pierwsza ma za zadanie pomnożyć liczbę typu int przez 2,
  • metoda druga ma za zadanie podzielić liczbę typu float przez 2,
  • metoda trzecia ma za zadanie zwrócić ten sam ciąg znaków z dodanym na końcu wykrzyknikiem,
  • metoda czwarta ma za zadanie zwrócić sumę liczb typu int przekazanych w tablicy,

a następnie wypisywał otrzymane wyniki.


Poniższy kod przedstawia przykladowy program JNIHelloWorld.java:

1: public class JNIHelloWorld {
2:
3:    	public native int metodaPierwsza(int i);
4:    	public native float metodaDruga(float f);
5:    	public native String metodaTrzecia(String s);
6:    	public native int metodaCzwarta(int[] arr);
7:    
8:    	public static void main(String[] args) {
9:		System.load("/home/babor/java/jni/libJNIHelloWorld.so");
10:		JNIHelloWorld hello = new JNIHelloWorld();
11:	
12:		int iWe = 10;
13:		int iWy;
14:		float fWe = 2.5f;
15:		float fWy;
16:		String sWe = "Witam! Tu kod natywny";
17:		String sWy;
18:		int[] arrWe = {1, 2, 3, 4};
19:		int sum;
20:	
21:		iWy = hello.metodaPierwsza(iWe);
22:		fWy = hello.metodaDruga(fWe);
23:		sWy = hello.metodaTrzecia(sWe);
24:		sum = hello.metodaCzwarta(arrWe);
25:	
26:		System.out.println("iWe = "+iWe+" iWy="+iWy);
27:		System.out.println("fWe = "+fWe+" fWy="+fWy);
28:		System.out.println(sWe);
29:		System.out.println(sWy);	
30:		System.out.println("arrWe.sum="+sum);
31:    }
32: }

Zdefiniowana zatem została klasa JNIHelloWorld zawierająca cztery metody natywne, zadeklarowane w liniach 3-6. Metody natywne są deklarowane w Javie poprzez dodanie słowa kluczowego native przed deklaracją metody, i nie zawierają one żadnej treści metody - nie mogą one być zaimplementowane, a jedynie zadeklarowane.

Linia 9 to wczytanie biblioteki współdzielonej, którą utworzymy za chwilę, zawierającej implementację zadeklarowanych metod natywnych. Można tu użyć metody System.load podając bezwzględną ścieżkę do biblioteki wraz z pełną nazwą pliku, lub metody System.loadLibrary podając jedynie nazwę biblioteki. W tym drugim przypadku jednak, biblioteka musi znajdować się w katalogu w którym Java przechowuje biblioteki (można to sprawdzić korzystając z polecenia System.getProperty("java.library.path")). Należy również pamiętać o zasadach nazewnictwa - w systemach Linux nazwy plików zawierających biblioteki współdzielone zaczynają się od lib i mają rozszerzenie .so, a zatem nasza biblioteka JNIHelloWorld powinna być utworzona pod nazwą libJNIHelloWorld.so (w przypadku polecenia loadLibrary podajemy tylko nazwę - JNIHelloWorld), w systemach Windows natomiast, biblioteki współdzielone mają rozszerzenia .dll, a zatem nasza biblioteka powinna zostać utworzona pod nazwą JNIHelloWorld.dll.

Linia 10 to utworzenie instancji naszej klasy. W liniach 12-19 zadeklarowane i zdefiniowane zostały zmienne uzywane w programie, natomiast linie 21-24 to wywołania natywnych metod. Jak zatem widać, metody te wewnątrz kodu Javy nie są w żaden sposó wyróżnione i wywoływane są poprzez odwołanie się do konkretnej metody istniejącego obiektu. Pobierają i zwracają typy danych w rozumieniu Javy - zarówno typy proste, jak i obiekty.

Linie 26-30 to wypisanie na ekran wyników działania programu.


Kompilacja programu w Javie

Zgodnie z planem tworzenia całości programu, gotowy kod Javy powinien zostać skompilowany. Wykonujemy to poleceniem:

java JNIHelloWorld.java

Jeżeli nie popełniliśmy żadnych błędów kod ten powinien się skompilować i w katalogu roboczym pojawi się binarny plik JNIHelloWorld.class.


Generowanie nagłówków metod natywnych

Możemy zatem przejść do następnego punktu planu i wygenerować plik zawierający nagłówki klas natywnych. Wykonujemy polecenie:

javah JNIHelloWorld

uzyskując w ten sposób plik JNIHelloWorld.h.

Treść tego pliku jest już nieco mniej czytelna - zawiera on nagłówki wszystkich metod natywnych zawartych w klasie JNIHelloWorld, które będą niezbędne do napisania ich treści w C/C++. Jak widać, deklaracje te różnią się znacząco od odpowiadających im deklaracji w Javie. JNIEXPORT oraz JNICALL to identyfikatory dla kompilatora dla eksporu funkcji. Typy danych, zaczynające się od litery j, to specyficzne typy języka C/C++, które są mapowane na typy Javy. Typ danych zwracany przez metodę znajduje się po identyfikatorze JNIEXPORT. Nazwy metod składają się z nazwy Java_, nazwy klasy w której dana metoda została zdefiniowana i nazwy metody, np. Java_JNIHelloWorld_metodaPierwsza. W argumentach metody, poza przekazywanymi zmiennymi, pojawiają się jeszcze JNIEnv * i jobject. Wskaźnik JNIEnv jest wskaźnikiem do przekazywanego środowiska (wskaźniki do funkcji itp.), natomiast jobject to referencja do obiektu zawierającego dane metody (w tym przypadku do obiektu hello).


Kod metod natywnych w C/C++

Mając gotowe nagłówki metod, możemy przystąpić do ich implementacji, a zatem do napisania ich kodu w C/C++. Jeśli chodzi o JNI, to składnia poleceń dla C i C++ jest nieco inna. Wynika to głównie z pewnych różnic w obsłudze obiektów i wskaźników. Przedstawiony poniżej kod będzie korzystać z funkcjonalności i składni języka C.

1: #include "JNIHelloWorld.h"
2: #include <string.h>
3: 
4: 
5: JNIEXPORT jint JNICALL Java_JNIHelloWorld_metodaPierwsza
6:     (JNIEnv *env, jobject obj, jint n) {
7:     return 2*n;
8: }
9: 
10: JNIEXPORT jfloat JNICALL Java_JNIHelloWorld_metodaDruga
11:     (JNIEnv *env, jobject obj, jfloat v) {
12:     return v/2;
13: }
14: 
15: JNIEXPORT jstring JNICALL Java_JNIHelloWorld_metodaTrzecia
16:     (JNIEnv *env, jobject obj, jstring string) {
17:     char buf[128];
18:     const jbyte *str = (*env)->GetStringUTFChars(env, string, 0);
19:     if(str == NULL) {
20:  		return NULL;
21:     }
22:     strcpy(buf,str);
23:     strcat(buf,"!");
24:     (*env)->ReleaseStringUTFChars(env, string, str);
25:     return (*env)->NewStringUTF(env, buf);
26: }
27: 
28: JNIEXPORT jint JNICALL Java_JNIHelloWorld_metodaCzwarta
29:     (JNIEnv *env, jobject obj, jintArray array) {
30:     int i, sum = 0;
31:     jsize len = (*env)->GetArrayLength(env, array);
32:     jint *elems = (*env)->GetIntArrayElements(env, array, 0);
33:	 if(elems == NULL) {
34:		return 0;
35:	 }
36:     for(i=0;i<len;i++) {
37: 		sum += elems[i];
38:     }
39:     (*env)->ReleaseIntArrayElements(env, array, elems, 0);
40:     return sum;	
41: }
42: 
43: int main(){}

W linii numer 1 dołączamy wygenereowany wczesniej plik z nagłówkami. Poniżej znajdują się implementacje metod. Linie 5-8 to metodaPierwsza - przekazywana jako argument liczba typu jint jest mnożona przez 2 i zwracana. Analogicznie w liniach 10-13, metodaDruga zwraca argument wejściowy typu jfloat podzielony przez 2.

metodaTrzecia pobierająca i zwracająca zmienne typu jstring jest już bardziej skomplikowana ze względu na złożony dostęp do ciągów znaków, które faktycznie są tablicami zmiennych znakowych. Typ jstring nie jest odpowiednikiem typu string w C/C++. Dostęp do zawartości zmiennych jstring w JNI polega na utworzeniu odpowiedniego wskaźnika do tablicy znaków przechowywanej w pamięci. W linii 18 tworzony jest wskaźnik i dane znakowe konwertowane są na typ zrozumiały dla C/C++. Po zakończeniu korzystania ze zmiennej jstring konieczne jest jeszcze jej "zwolnienie" (linia 24), w przeciwnym wypadku mogą pojawić się błedy związane z wyciekami pamięci.

Analogiczny dostęp konieczny jest do tablic. metodaCzwarta w liniach 28-41 pobiera jako argument tablicę typu jintArray, a więc tablicę zmiennych typu prostego jint. Aby uzyskać dostęp do jej elementów ponownie należy skorzystać ze specjalnych funkcji - linia 31 pobiera długość tablicy i zapisuje ją w zmiennej typu jsize (analogiczna do jint), a linia 32 tworzy bezpośredni wskaźnik do elementów tablicy. Ponownie aby uniknąć błędów pamięciowych, tablica musi zostać "zwolniona" (linia 39).

Posiadanie bezpośredniego wskaźnika do danych tablicowych pozwala również na modyfikację danych bezpośrednio w pamięci, a co za tym idzie - w zmiennej wejściowej. Wówczas po jej "zwolnieniu" zmiany będą również widoczne w Javie.


Kompilacja biblioteki współdzielonej

Mając gotowy kod metod natywnych napiscany w C musimy go skompilować jako bibliotekę współdzieloną. Mając do dyspozycji Linux'owy kompilator gcc możemy to zrobić wydając polecenie:

gcc -I$JDK_PATH/include/ -I$JDK_PATH/include/linux/ -shared JNIHelloWorld.c -o libJNIHelloWorld.so  

gdzie $JDK_PATH to ścieżka dostępu do katalogu zawierającego instalację JDK.

O ile podaliśmy poprawnie ścieżki do dołączanych bibliotek i plików, kompilacja powinna przebiec pomyślnie i w katalogu roboczym powinien pojawić się plik libJNIHelloWorld.so.


Uruchomienie programu

Do uruchomienia stworzonego przez nas programu konieczne są dwa pliki - JNIHelloWorld.class oraz libJNIHelloWorld.so. Pierwszy z nich to skompilowany kod Javy korzystający z metod natywnych, a drugi to biblioteka współdzielona zawierająca metody natywne.

Program uruchamiamy w wirtualnej maszynie Javy poleceniem: java JNIHelloWorld

Powinniśmy otrzymać następujący wynik: iWe = 10 iWy=20 fWe = 2.5 fWy=1.25 Witam! Tu kod natywny Witam! Tu kod natywny! arrWe.sum=10

Często pojawiającym się problemem przy uruchamianiu programu korzystającego z JNI jest błąd: Exception in thread "main" java.lang.UnsatisfiedLinkError: Can't load library. Oznacza to, że niemożliwe jest wczytanie biblioteki współdzielonej zawierającej metody natywne. Należy wówczas zwrócić szczególną uwagę na metodę wczytującą System.load lub System.loadLibrary i podawaną nazwę biblioteki, oraz na lokalizację pliku z biblioteką.


Na koniec...

W następnym numerze biuletynu umieszczona zostanie druga część tego artykułu opisująca wywoływanie kodu Javy z C/C++.

Dla zainteresowanych bardziej szczegółowym zgłębieniem tematu, lub wybiegnięciem do tematyki nastepnej części artykułu, już teraz kilka przydatnych i ciekawych linków do materiałów dotyczących JNI:


Porady: Pomiar czasu wykonania programu

Autor: Michał Łopuszyński

Po co to wszystko?

Tworząc własne oprogramowanie obliczeniowe, często chcielibyśmy porównać efektywność działania kilku różnych jego wersji np. implementujących różne algorytmy, skompilowanych z innymi opcjami kompilacji, korzystających z kilku wersji bibliotek itp. Do tego celu bardzo przydatna jest możliwość pomiaru czasu wykonania programu. W poniższym artykule opiszemy jak można to zrobić. Zaczniemy od najprostszej możliwości - wykorzystania uniksowego narzędzia time. Następnie pokażemy jak dodać do własnego programu wywołania funkcji bibliotecznych, dzięki którym będzie możliwe sprawdzenie, ile czasu procesor spędza w poszczególnych częściach kodu.

Najprostsze narzędzie - time

Spróbujmy pobawić się trochę programem time:

halo# /usr/bin/time ./test.x
14.72user 0.75system 0:15.85elapsed 97%CPU (0avgtext+0avgdata 0maxresident)k
0inputs+0outputs (0major+152minor)pagefaults 0swaps

No ładnie, mamy wynik - nasz program wykonał się w 15.85 sekundy, są też informacje o czasie rzeczywiście wykorzystanym przez proces i system, wykorzystaniu CPU, pamięci, obszaru swap. Może wolelibyśmy trochę czytelniejszy format? Proszę bardzo:

halo# /usr/bin/time -p ./test.x
real 15.16
user 14.62
sys 0.39

Na tym jednak nie koniec możliwości, dokładne informacje wypisywane przez time możemy sobie skonfigurować przy pomocy opcji -f FORMAT. Jako parametr FORMAT podajemy napis ze specjalnymi flagami rozpoczynającymi się znakiem %, np.

halo# /usr/bin/time -f "Czas wykonania %E, wykorzystanie CPU %U, zakonczony ze statusem %x" ./test.x
Czas wykonania 0:15.44, wykorzystanie CPU 96%, zakonczony ze statusem 0

Dokładny opis wszystkich dostępnych flag możliwych do wykorzystania w FORMAT można znaleźć na stronach manuala ( man time ).

Wyniki działania polecenia time trafiają zwykle na standardowy strumień błędu (stderr), co nie zawsze jest wygodne. Wyniki możemy skierować do wybranego przez nas pliku opcją -o NAZWA_PLIKU .

Uwaga! time występuje w dwóch wariantach - jako wewnętrzne polecenie powłok rodziny csh i jako zewnętrzne narzędzie. Wersja wbudowana w csh ma trochę uboższe możliwości. Chcąc upewnić się, że wykorzystujemy wersje zewnętrzną najlepiej podawać pełną ścieżkę do programu time, jak w przykładach powyżej.

Pomiar czasu wewnątrz programu

Czasami jednak pomiar całości wykorzystanego czasu CPU nie wystarcza. Często przecież chcielibyśmy dowiedzieć się ile czasu program spędza w poszczególnych fragmentach zaprogramowanego algorytmu. Praktycznym rozwiązaniem jest uzupełnienie własnego programu o kilka linijek, które zmierzą czas wykonania wybranej części kodu.

Kod do pomiaru czasu - C

Przykładowy fragment programu w języku C jest przedstawiony poniżej:

#include

Oczywiście nic nie stoi na przeszkodzie, aby we własnym programie umieścić kilka tego typu wstawek i dzięki temu uzyskać dokładniejsze informacje o czasach wykonania różnych części naszego programu.

Kod do pomiaru czasu - Fortran

Analogiczny fragment w języku Fortran przedstawia się następująco:

       program timer_1
  
       real time_start, time_stop
 
       call cpu_time(time_start)
 
       call do_work()
 
       call cpu_time(time_stop)
  
       write(*,*) "Wynik pomiaru czasu w sekundach:", time_stop-time_start 
 
       end

Wykorzystana procedura cpu_time jest częścią standardu języka Fortran 95, więc wszystkie nowsze kompilatory powinny ją udostępniać. Innymi popularnymi funkcjami do pomiaru czasu, dostępnymi na wielu platformach są na przykład:

  • seconds(), która zwraca liczbę typu real zwykle związana z czasem użytkownika. Przykład wykorzystania wygląda następująco:
       program timer_2
  
       real time_start, time_stop
 
       time_start=second()
 
       call do_work()
 
       time_stop=second()
  
       write(*,*) "Wynik pomiaru czasu w sekundach:", time_stop-time_start 
 
       end
  • etime(times), która do dwuelementowej tablicy liczb rzeczywistych times podanej jako parametr, wpisuje odpowiednio czas użytkownika i systemowy, a sumę powyższych dwóch wielkości zwraca jako wynik. Oto przykładowy kod:
       program timer_3
     
       real time_start, time_stop
   
       real times_start(2), times_stop(2)
 
       time_start=etime(times_start)
 
       call do_work()
 
       time_stop=etime(times_stop)

       write(*,*) "Wynik pomiaru czasu w sekundach:", time_stop-time_start 
 
       end

Kod do pomiaru czasu - MPI

W oprogramowaniu wykorzystującym bibliotekę MPI, można wykorzystać funkcję MPI_Wtime(), zwraca ona bieżące wskazanie zegara w sekundach jako liczbę podwójnej precyzji. Jej użycie jest analogiczne jak w poprzednich omówionych przykładach. Ważną rzeczą jest aby, w przypadku programów równoległych, unikać wyznaczania czasów wykonania na podstawie różnicy wyników MPI_Wtime() otrzymanych na różnych procesorach. Biblioteka MPI nie gwarantuje, że czas podawany przez MPI_Wtime() jest globalny.

Podsumowanie

W powyższym artykule omówiliśmy najbardziej podstawowe metody pomiaru czasu wykonania własnych programów. Oczywiście w praktyce programistycznej często okazuje się, że opisane proste metody nie wystarczają. Wtedy trzeba skorzystać z wyspecjalizowanych narzędzi do pomiaru wydajności oprogramowania, czyli tzw. profilerów. Przykładami takich narzędzi mogą być np. gprof, pgprof, opt i wiele innych. Ich wykorzystanie to już jednak temat na osobny artykuł.


Forum dla użytkowników KDM

Autor: Maciej Cytowski

Przygotowaliśmy dla Państwa forum, które ma służyć interaktywnej wymianie informacji pomiędzy użytkownikami oraz pracownikami działu KDM. Forum dostępne jest na stronie: http://www.icm.edu.pl/kdmforum.

Mechanizm forum oparty jest na znanym narzędziu phpBB. Podstawowy podręcznik użytkownika dostępny jest tutaj.


Z forum korzystać mogą wszystkie osoby posiadające aktualne konto w ICM. Do logowania należy użyć swojego ICM-owego loginu i hasła.

Struktura forum może na początku ulegać drobnym zmianom. Aktualnie dostępne są następujące kategorie:


  • Poradnik użytkownika - miejsce w którym będzie można znaleźć porady dotyczące podstawowych problemów początkujących użytkowników ICM. W dziale tym można również proponować tematy artykułów Biuletynu KDM dotyczące rozwiązywania bardziej złożonych problemów, które napotkali Państwo podczas pracy na komputerach ICM.


  • Klaster Halo - miejsce do zgłaszania problemów oraz wymiany informacji dotyczących pracy na klastrze halo (system kolejkowy, narzędzia programistyczne, narzędzia systemowe).


  • Komputer Tornado (Cray X1e) - miejsce do zgłaszania problemów oraz wymiany informacji dotyczących pracy na komputerze tornado (system kolejkowy, narzędzia programistyczne, narzędzia systemowe).


  • Oprogramowanie - miejsce do zadawania pytań oraz zamieszczania uwag dotyczących korzystania z oprogramowania naukowego zainstalowanego na komputerach ICM.


  • Obsługa projektów - miejsce do zadawania pytań dotyczących obsługi projektów.


  • Inne - miejsce na ogłoszenia i uwagi.


Mamy nadzieję, że ten sposób komunikacji pomiędzy użytkownikami upowszechni się w naszym centrum. Często zdarza się, że problemy przed którymi Państwo stają na co dzień podczas pracy na komputerach ICM były już rozwiązane wcześniej przez użytkownika bądź pracownika ICM. Wówczas forum jako składnica wiedzy tajemnej może pomóc Państwu zaoszczędzić trochę jakże cennego czasu.

Zapraszamy do korzystania!