Z doby svých studií si pamatuji, že datový typ float není moc dobrý na součty. A double je na tom lépe jen o chlup.
Pokud provedu součet čísel od 1 do 200000, tak je součet 20000100000. To je 11 číslic, což float v Javě nezvládne. Nemá dost velké rozlišení, takže začne zaokrouhlovat a ztrácet několik spodních číslic.
Zda k tomu došlo i u příkladu z článku nevím úplně jistě, protože nevím, zda 64bitová Java má float jen 32bitový, nebo také 64bitů (předpokládám, že nikoliv, protože v dokumentaci se odkazuje na float = IEEE 754 Single Precision). Ostatně výsledek výpočtu 100001.3 neodpovídá tomu, co bych jako správný výsledek očekával (průměr od 1 do 200000 je 100000.5)
Uznávám, že na samotný obsah článku to nemá vliv, jen mě překvapilo, že se tu takový příklad objevil.
Je to tak a jsem rad, ze si toho nekdo vsiml (coz znamena, ze u vas na fakulte to asi nekdo vysvetlil, coz kupodivu neni zvykem vsude :-).
U FP obecne neplati napriklad vztah (a+b)-b = a + (b-b) atd., tady to urcite vede k pocetni chybe.
Ja jsem pouzil float[] schvalne, a to z toho duvodu, aby se mohl jak JIT, tak i GCC "vyradit" na pouziti SSE, nicmene neni problem to prepsat na pouziti longu.
Jinak typ float je v Jave vzdy IEEE 754 single, konkretni vysledky operaci se sice mohou lisit, ale to resi modifikator "strictfp".
Chápu pokus o relativní jednoduchost příkladů. Ale je trochu nefér porovnávat nativní kód bez optimalizace na moderní procesor s optimalizací na moderní procesor přes JIT. Samozřejmě, test je poměrně dost syntetický a asi nedává příliš smysl řešit složitě inicializaci různých kompilovaných knihoven podle procesoru na tomto příkladu. Nicméně kompilace pro procesory s MMX a SSE instrukcemi by mohl celkem pomoct. Zkuste si schálně přidat k GCC parametrům ještě -march=native. Sice se tento kód tímto stane nepřenosným, ale zase překladač bude moci využít plně možností daného CPU, stejně jako to může JIT.
Pravda, prakticky se asi vyplatí buildovat tak 2 verze. Jednu pro moderní procesory s podporou několika rozšíření a jednu záložní, co bude šlapat na všem. Potom ozkoušet, jestli přínos pro výpočet vůbec stojí za tu námahu.
Každopádně díky za upozornění, že nativní kód sám o sobě zrychlení nepřináší automaticky. Ne, že by to bylo překvapující, ale je dobré mít na to čísla. Ještě si sám ověřím čísla pro různé optimalizace. (Základ, i686, presscot, core2... :))
Diky za poznamku, je to samozrejme pravda, ale tento priklad byl puvodne ziskan ze skutecne aplikace, ktera se snazila provadet operace nad obrazky (byte[][] a float[][]) v nativnim kodu, coz ale nevedlo k prakticky zadnemu urychleni prave kvuli neustalym kopiim poli.
A pouziti vice verzi knihoven bylo prakticky nemozne kvuli nutnosti udrzovat aplikaci a jeji instalace (uz tak dodavat ctyri verze bylo skoro nad sily vyvojaru - linux i686/x86_64 + Windows to same).
Pokud si budete vysledky overovat, slo by je prosim potom pridat do diskuze? Sam jsem zvedavy, jestli se vubec neco podari urychlit, resp. jestli to bude poznat na vysledcich.
Jestli ona to neni cela podstata JITu. Bud to napisete jednou dost dobre a nechate JIT aby si s tim poradil na vsemoznych platformach, nebo se rozhodnete pro nativni a pak budete s neskutecnym nasazenim udrzovat neskutecne mnozstvi binarek pro vsechny podporovane kombinace OS+HW (a to navic v case do minulosti i budoucnosti). Pokud to neudelate, tak se lehce muze stat ze ten program bude za pet let rychlejsi v ciste jave nez v nativu a cele to usili prijde vnivec.
"Každopádně díky za upozornění, že nativní kód sám o sobě zrychlení nepřináší automaticky. "
Hmm téměř každý den slyším pravý opak, protože mnoho takyprogramátorů když slyší Java, tak si to automaticky spojí s JVM ještě v před-hotspotových dobách a když slyší GCC tak si to automaticky spojí s céčkovými programy psanými nějakými génii. Ani jedno dnes už neplatí, ale zažité zvyky se mění pomalu žejo...
Taky jsem s s tím hrál(cca před půl rokem) ale *.dll jsem vytvořil v obyčejném Cčku s kompilátorem MinGW. Přenášel jsem nikoliv pole a ale přímo matice,objekty a rozdíl byl skutečně viditelný.
Stejný algorytmus se stejnou úlohou(načteni obou matic ze souboru) ( notas; 2jádroAMD;2,3Ghz)
čistě Cčko .... 1,3 hodiny
Java s JNI ..... 1,4 hodiny
čistě Java ..... 2,1 hodiny
nasobení matic 5000x5000(kvůli zjednodušení plné generovanýc h integerů)
Prostě jsem měl dvě matice každá 5000x5000, plná random integerů a nasobil je mezi sebou. Tak jsem se na to podrobně podíval a zde jsou vysledky co jsem si kvůli závěrům ponechal:
- za pomoci vlákem jsem to nedělal( moc práce ), přeci jenom programuji rok a půl. A ani nevím jestli Cčko vlákna má a C++ neumím.
// JAVA
// souborA .... 500/500 a souborB 500/500 = cca 2,3 sekund 2 382 milisekund
// souborA .... 1000/1000 a souborB 1000/1000 = cca 24 sekund 24 124 milisekund
// souborA .... 2000/2000 a souborB 2000/2000 = cca 274 sekund 274 963 milisekund
// souborA .... 5000/5000 a souborB 5000/5000 = cca 6245 sekund 6 245 725 milisekund
// JAZYK C
// souborA .... 500/500 a souborB 500/500 =
// souborA .... 1000/1000 a souborB 1000/1000 = cca 22,4 sekund 22 400 milisekund
// souborA .... 2000/2000 a souborB 2000/2000 = cca 296 sekund 296 000 milisekund
// souborA .... 5000/5000 a souborB 5000/5000 = cca 3937 sekund 3 937 470 milisekund
// JAVA pres DLL(nasobeni pres C)
// souborA .... 500/500 a souborB 500/500 = cca 4,3 sekund 4 230 milisekund
// souborA .... 1000/1000 a souborB 1000/1000 = cca 30 sekund 30 000 milisekund // souborA .... 2000/2000 a souborB 2000/2000 = cca 275 sekund 275 070 milisekund // souborA .... 5000/5000 a souborB 5000/5000 = cca 4194 sekund 4 194 160 milisekund
metoda v Jave :
public native int [][] vystupIntPole(int [][] pole1,int velikost1,int [][] pole2,int velikost2);
header z C:
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class NasobeniMatic */
#ifndef _Included_NasobeniMatic
#define _Included_NasobeniMatic
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: NasobeniMatic
* Method: vystupIntPole
* Signature: ([[II[[II)[[I
*/
JNIEXPORT jobjectArray JNICALL Java_NasobeniMatic_vystupIntPole
(JNIEnv *, jobject, jobjectArray, jint, jobjectArray, jint);
#ifdef __cplusplus
}
#endif
#endif
nasobíci algorytmus v Cčku:
// PROVEDEME NASOBENI MATIC
startNasob=clock();
for(i=0;i<pocetRadkuA;i++) { // cyklus pro nasobeni matic
for(j=0;j<pocetSloupcuB;j++) {
matrixC[i][j] = 0;
for(k=0;k<pocetSloupcuB;k++) { // pri chybach v nasobeni zkusit zmenit pocetRadkuA
matrixC[i][j]=matrixC[i][j] + matrixA[i][k] * matrixB[k][j]; } } }
konecNasob = clock();
Princi programů byl následující :
1) načíst matici 1 ze souboru ( zjistil cas jak dlouho to dělá)
2) načíst matici 2 ze souboru ( zjistil cas jak dlouho to dělá)
3) provést nasobení + na konci odečíst čas ziskaní před nasobením
4) toť vše