Wydajność Javy a C++

Odpowiedz Nowy wątek
2020-01-13 11:18
1

Cześć, zrobiłem prosty test. Porównałem czasy wykonywania się programów napisanych w C++ oraz w Javie. W obu programach został napisany algorytm obliczania pierwiastka kwadratowego z wykorzystaniem metody Herona. Z tego co wiem to C++ używa się do aplikacji w których kluczową rolę pełni wydajność. Zawsze myślałem, że Java jest wolniejsza, ale ma bardzo dużo zastosowań, jednak z tego co mi wyszło to jednak taka wolna nie jest.
program w Javie:

public class Test {
    public static void main(String[] args) {
        double square=0d;
        long before=0l, now=0l;
        before = System.nanoTime();
        square = squareRoot(90, 100000000, 20);
        now = System.nanoTime();
        System.out.println("SquareRoot:  " + square + ", elapsed time: " +(now-before)/1000000d + "ms.");

    }

    static double squareRoot(double number, int iteration, double condition) {
        double xi=condition;
        double xi1=0.0;

        for (int i=0; i<iteration; i++) {
            xi1 = 1.0/2.0 * (xi + number/xi);
            xi = xi1;
        }

        return xi1;
    }
}

Program w C++:

double squareRoot(double number, int iteration, double condition) {
    double xi=condition;
    double xi1=0.0;

    for (int i=0; i<iteration; i++) {
    xi1 = 1.0/2.0 * (xi + number/xi);
    xi = xi1;
    }

    return xi1;
}

int main()
{
    double square = 0;
    auto start = chrono::steady_clock::now();

    square = squareRoot(90, 100000000, 20);

    auto end = chrono::steady_clock::now();

    cout << "Elapsed time in milliseconds : "
        << chrono::duration_cast<chrono::milliseconds>(end - start).count()
        << " ms, and squareRoot is: " <<square<< endl;

    cout << "Elapsed time in seconds : "
        << chrono::duration_cast<chrono::seconds>(end - start).count()
        << " sec, and squareRoot is: " <<square;

    return 0;
}

Dla 100000000 iteracji, do obliczenia pierwiastka kwadratowego z liczby 90 z warunkiem początkowym 20 obliczenia w Javie zajęły średnio 815 ms. Dla C++ było to średnio 1350 ms czyli bardzo duża różnica. Skąd taka rozbieżność?

edytowany 1x, ostatnio: infantylny, 2020-01-13 11:18

Pozostało 580 znaków

2020-01-13 11:21
0

A włączyłeś optymalizacje w C++ podczas kompilacji?


Nie ogarniam za bardzo C++, co masz na myśli mówiąc optymalizacja C++ podczas kompilacji? - infantylny 2020-01-13 11:27
Odpowiadaj w odpowiedziach. - kq 2020-01-13 11:29
Dla clanga są takie flagi dla włączania różnego rodzaju optymalizacji. Dla gcc są podobne. Ogólnie przetestuj jeszcze raz z flagą -O3 - Kamil Żabiński 2020-01-13 11:29

Pozostało 580 znaków

2020-01-13 11:32
0

Nie ogarniam za bardzo C++, co masz na myśli mówiąc optymalizacja C++ podczas kompilacji?

To znaczy zbuduj w trybie release z optymalizacją O3 jeśli używasz kompilatora g++ lub O2 jeśli budujesz z użyciem kompilatora od microsoftu.

Z tego co wiem to C++ używa się do aplikacji w których kluczową rolę pełni wydajność

Tak. Szkopuł w tym, że trzeba wiedzieć w jaki sposób C++ użyć żeby tą wysoką wydajność otrzymać. Jeśli chodzi o pierwiastek kwadratowy to od lat 90 stosuje się różne haki żeby przyspieszyć obliczenia.

edytowany 2x, ostatnio: several, 2020-01-13 11:36

Pozostało 580 znaków

2020-01-13 11:38
0

screenshot-20200113113814.png

Włączyłem i dalej jest to samo

Pozostało 580 znaków

2020-01-13 11:44
0

To znaczy, że użyty przez Ciebie algorytm jest szybszy w Javie w Twoim środowisku. Teraz spróbuj innego algorytmu, np Babylonian method. Jeśli nadal Java będzie szybsza i będziesz się zastanawiał po co ludziom C++ to wiedz, że z poziomu C++ łatwo można użyć sprzętowego fsqrt, który zgaduję, że będzie najszybszy.

EDIT
Kurczę już widzę, jak @katelx albo @Wibowit tu wparowuje, żeby mnie z ziemią zrównać. Wiem, że w Javie można pisać mega szybkie apki, nie trzeba mnie uświadamiać.

edytowany 3x, ostatnio: several, 2020-01-13 11:54
Oh shit, here we go again :D - several 2020-01-13 12:06

Pozostało 580 znaków

2020-01-13 12:07
1

Szybciej działa, po zlikwidowaniu nawiasu w pętli (u mnie ok. 15%), po skompilowaniu z optymalizacjami (-O3):
Java: 788.80936ms;
C++: 737 ms.
Z takim krótkim kodem JIT sobie radzi równie dobrze, jak gcc, i w sumie to nie ma co się dziwić.
@infantylny, jaki Masz kompilator C++? U mnie: g++-8 -std=c++17 -O3 -Wall -pedantic

EDYCJA: Dla klarowności kompilowany kod w pętli:

    xi1 = 1.0/2.0 * xi + 1.0/2 * number/xi;
    xi = xi1;

edytowany 1x, ostatnio: lion137, 2020-01-13 12:19
Który nawias zlikwidowałeś? - Patryk27 2020-01-13 12:16

Pozostało 580 znaków

2020-01-13 12:18
0

Dobra, a jaki będziesz miał wynik jak do tej funkcji zmienna i będzie typu register?

double squareRoot(double number, int iteration, double condition) {
    double xi=condition;
    double xi1=0.0;

    register int i;
    for (i=0; i<iteration; i++) {
    xi1 = 1.0/2.0 * (xi + number/xi);
    xi = xi1;
    }

    return xi1;
}

edytowany 1x, ostatnio: goose_, 2020-01-13 12:18
Wątpie żeby cokolwiek to zmieniło. Zresztą godbolt prawdę ci powie, z O3 dostajesz ten sam asembler. - Shalom 2020-01-13 12:25
Serio? Ostatnio widziałem w telewizji jak świetowali 2020. To chyba dobry rok, żeby nie musieć kompilatorowi c++ (nawet gcc) pisać co ma wrzucać w rejestry. - jarekr000000 2020-01-13 12:37
@jarekr000000 auć. I to o raku niżej... tak jestem amatorem i się nie znam. Obejrzałem Twoje wykłady o java i springu, więc... znasz się na tym lepiej ode mnie. Wybacz mi moją niewiedzę i dzięki za lekcje pokory. - goose_ 2020-01-13 13:13
@goose_: wrzuć oba kody do https://godbolt.org/ daj w parametrach O3 i spróbuj coś ręcznie zoptymalizować tak, żeby wyszedł ci inny kod ;) - Shalom 2020-01-13 13:21
@Shalom Widzę że kompilator przy -O3 wywalił chyba działanie xi = xi1; uznając że xi nie jest istotne, nie robi nic prócz zapisywania do zmiennej lokalnej ale ona nie jest używana. Więc przyznaję się że z obecną wiedzą nie robiłbym takiej optymalizacji tylko szukał instrukcji które wykonają szybko ten kod. Poza tym nie wiem co zmienić w funkcji squareRoot żeby to było szybsze niż przy -O3. W każdym razie dzięki za wskazówkę, bo zmusiłeś mnie trochę do wysiłku żeby to sprawdzić i zweryfikowałem swoją dotychczasową wiedzę na ten temat. - goose_ 2020-01-13 14:12
@goose_: Ostrożnie z tą pokorą - ktoś coś musi robić w końcu, relax. Tony ludzi, całkiem wykształconych i doswiadczonych powrzucało dużo bzdur do sieci. łącznie ze mnną. Benchmarki to świnie niestety, a mikro-optymalizacjke skończyły się na 6502. Kłopot, że łatwo wrzucić, trudno uwalić, tony bzdurnych pomysłów krąży i ludzie powtarzają te bzdury przez 20 lat. Mój tekst poniżej odnosił się do większej ilości postów, ale ładnie ogarnął to Koziołek (trzeba było chwilę pracy). - jarekr000000 2020-01-13 15:16

Pozostało 580 znaków

2020-01-13 12:25
10

Zawsze myślałem, że Java jest wolniejsza

To nie takie proste ;)
Java generalnie może być wolniejsza, bo wymaga interpretowania bytecode'u w runtime, ale z drugiej strony dzięki temu może optymalizować kod nie tylko w czasie kompilacji (jak C) ale też w czasie wykonania (tzw. JIT). W efekcie w zależności od sytuacji, może się okazać szybsza.

Przykład poglądowy: wyobraź sobie, że masz program który wiele razy wykonuje dzielenie x/y. Na poziomie kompilacji C nie wiele tu może wyczarować, ot na poziomie asemblera będzie tam div. Ale wyobraź sobie, że uruchamiasz taki program z y=2. Teraz takie dzielenie można by wykonać dużo szybciej jednym shiftem, zamiast kosztownego diva. No ale dla C jest już za późno, kod już wygenerowany i tylko się wykonuje. Ale dla JITa może nie być jeszcze za późno, bo on działa w runtime i może wygenerować zoptymalizowany kod maszynowy w trakcie działania programu.


Masz problem? Pisz na forum, nie do mnie. Nie masz problemów? Kup komputer...

Pozostało 580 znaków

2020-01-13 12:41
2

Szkoda, że akurat jestem zasypany pracą. Fajny rak w niektórych postach. Taki nie za łatwy do usunięcia.

@Shalom true, ale warto dodać, że od jakiegoś czasu w standardzie można kompilować javę do native, statycznie... i taki kod zwykle jest wolniejszy w dłuższej perspektywie, bo nie ma tych jitowych optymalizacji. (Ale programik szybciej startuje, wiec czasem ma to sens).


Bardzo lubie Singletony, dlatego robię po kilka instancji każdego.
edytowany 3x, ostatnio: jarekr000000, 2020-01-13 12:49
Dopiero co przepisywaliśmy pythonowy skrypt na C++, właśnie dlatego, że wystartowanie pythona i zrobienie kilku importów trwało kilka razy dłużej niż faktyczna praca tego skryptu, więc tak, czas startowania często może być dużo istotniejszy niż jakieś fikuśne optymalizacje, które nigdy nie wystąpią :) - Shalom 2020-01-13 13:50

Pozostało 580 znaków

2020-01-13 12:48
0

U mnie czas wyszedł jednakowy (różnica nieistotna):
https://wandbox.org/permlink/WtAuAW7PncjFZqEo
https://wandbox.org/permlink/296GfkgSwBVKgE6F

Co do tłumaczenia bytcode -> kod maszynowy wykorzystujący w pełni procesor:
dobranie właściwej architektury w C++ daje pomijalnie mały zysk: https://wandbox.org/permlink/4HbGXqg2rdPIjYBh (dodatkowy przełącznik: użyj instrukcji specyficznych dla bieżącego procesora).


Jeśli chcesz pomocy, NIE pisz na priva, ale zadaj dobre pytanie na forum.
edytowany 5x, ostatnio: MarekR22, 2020-01-13 12:52

Pozostało 580 znaków

2020-01-13 12:56

No tak mierząc czas, to daleko nie zajedziemy. Z kilku powodów, a najważniejszy to błędy zegara oraz sposób dostępu do wyjścia.

public class Test {
   public static void main(String[] args) {
      double square=0d;
      long before=0l, now=0l;
      square = squareRoot(90, 100000000, 20);
  }

  static double squareRoot(double number, int iteration, double condition) {
      double xi=condition;
      double xi1=0.0;
      for (int i=0; i<iteration; i++) {
         xi1 = 1.0/2.0 * (xi + number/xi);
         xi = xi1;
     }
     return xi1;
  }
}
#include <iostream>  
#include <chrono>     

using namespace std; 

double squareRoot(double number, int iteration, double condition) {

  double xi=condition;
  double xi1=0.0;
  for (int i=0; i<iteration; i++) {
   xi1 = 1.0/2.0 * (xi + number/xi);
   xi = xi1;
  }
  return xi1;
}

int main()
{
   double square = 0;
   square = squareRoot(90, 100000000, 20);
   return 0;
}

W ten sposób eliminujemy z pomiaru błędy związane z czasem działania programu. W przypadku javy należy dorzucić jeszcze czas potrzebny na uruchomienie JVM, więc pomiary in-code raczej nie będą miarodajne. Proponuję więc inne podejście i użycie pomiarów out-code z użyciem unixowego time. Realizowane wg poniżeszego scenariusza:

#!/bin/bash

echo 'Informacje o g++'
g++ -v

echo -e "\n"

echo 'Informacje o Javac i Java'
javac -version
java -version

echo -e "\n"
echo 'Program w C++'
echo 'Kompilacja bez opcji'

time g++ test.c
echo 'Uruchomienie programu w C++'
time ./a.out

echo -e "\n"
echo 'Program w Java'
echo 'Kompilacja'
time javac Test.java
echo 'Uruchomienie programu w Java'
time java Test

echo -e "\n"
echo -e "Usuwam pliki wykonywalne"
rm a.out Test.class

echo -e "\n"
echo 'Kompilacja w C++ z użyciem opcji -O3'
time g++ -O3 test.c
echo 'Uruchomienie programu w C++ skompilowanego z opcją -O3'
time ./a.out

Wyniki

Informacje o g++ 
Using built-in specs.
COLLECT_GCC=g++
COLLECT_LTO_WRAPPER=/usr/lib/gcc/x86_64-linux-gnu/7/lto-wrapper
OFFLOAD_TARGET_NAMES=nvptx-none
OFFLOAD_TARGET_DEFAULT=1
Target: x86_64-linux-gnu
Configured with: ../src/configure -v --with-pkgversion='Ubuntu 7.4.0-1ubuntu1~18.04.1' --with-bugurl=file:///usr/share/doc/gcc-7/README.Bugs --enable-languages=c,ada,c++,go,brig,d,fortran,objc,obj-c++ --prefix=/usr --with-gcc-major-version-only --program-suffix=-7 --program-prefix=x86_64-linux-gnu- --enable-shared --enable-linker-build-id --libexecdir=/usr/lib --without-included-gettext --enable-threads=posix --libdir=/usr/lib --enable-nls --with-sysroot=/ --enable-clocale=gnu --enable-libstdcxx-debug --enable-libstdcxx-time=yes --with-default-libstdcxx-abi=new --enable-gnu-unique-object --disable-vtable-verify --enable-libmpx --enable-plugin --enable-default-pie --with-system-zlib --with-target-system-zlib --enable-objc-gc=auto --enable-multiarch --disable-werror --with-arch-32=i686 --with-abi=m64 --with-multilib-list=m32,m64,mx32
--enable-multilib --with-tune=generic --enable-offload-targets=nvptx-none --without-cuda-driver --enable-checking=release --build=x86_64-linux-gnu --host=x86_64-linux-gnu --target=x86_64-linux-gnu
Thread model: posix
gcc version 7.4.0 (Ubuntu 7.4.0-1ubuntu1~18.04.1)

Informacje o Javac i Java
javac 11.0.5
openjdk version "11.0.5" 2019-10-15
OpenJDK Runtime Environment AdoptOpenJDK (build 11.0.5+10)
OpenJDK 64-Bit Server VM AdoptOpenJDK (build 11.0.5+10, mixed mode)

Program w C++
Kompilacja bez opcji

real    0m0,236s
user    0m0,205s
sys     0m0,030s
Uruchomienie programu w C++

real    0m0,812s
user    0m0,811s
sys     0m0,000s

Program w Java
Kompilacja

real    0m0,413s
user    0m0,810s
sys     0m0,072s
Uruchomienie programu w Java

real    0m0,608s
user    0m0,629s
sys     0m0,000s

Usuwam pliki wykonywalne

Kompilacja w C++ z użyciem opcji -O3

real    0m0,209s
user    0m0,196s
sys     0m0,013s
Uruchomienie programu w C++ skompilowanego z opcją -O3

real    0m0,001s
user    0m0,001s
sys     0m0,000s

Wygląda lepiej, prawda?
Ale coś tu jest nie tak…

GCC w czasie kompilacji z opcją -O3 wylicza, że wynik squareRoot jest nieużywany i można go usunąć. Przepisujemy zatem nasze programy do postaci:

public class Test {
   public static void main(String[] args) {
      double square=0d;
      long before=0l, now=0l;
      square = squareRoot(90, 100000000, 20);
      System.out.println(square);
  }

  static double squareRoot(double number, int iteration, double condition) {
      double xi=condition;
      double xi1=0.0;
      for (int i=0; i<iteration; i++) {
         xi1 = 1.0/2.0 * (xi + number/xi);
         xi = xi1;
     }
     return xi1;
  }
}
#include <iostream>  
#include <chrono>     

using namespace std; 

double squareRoot(double number, int iteration, double condition) {

  double xi=condition;
  double xi1=0.0;
  for (int i=0; i<iteration; i++) {
   xi1 = 1.0/2.0 * (xi + number/xi);
   xi = xi1;
  }
  return xi1;
}

int main()
{
   double square = 0;
   square = squareRoot(90, 100000000, 20);
   cout << square;
   return 0;
}

i teraz porównajmy wyniki:

Program w C++
Kompilacja bez opcji

real    0m0,239s
user    0m0,203s
sys     0m0,036s
Uruchomienie programu w C++
9.48683
real    0m0,800s
user    0m0,798s
sys     0m0,000s

Program w Java
Kompilacja

real    0m0,416s
user    0m0,827s
sys     0m0,086s
Uruchomienie programu w Java
9.486832980505138

real    0m0,605s
user    0m0,615s
sys     0m0,016s

Usuwam pliki wykonywalne

Kompilacja w C++ z użyciem opcji -O3

real    0m0,229s
user    0m0,197s
sys     0m0,026s
Uruchomienie programu w C++ skompilowanego z opcją -O3
9.48683
real    0m0,545s
user    0m0,544s
sys     0m0,000s

I teraz czasy są już poprawne. Co zatem robi -O3? Ano w takim prostym programie nie musi nic robić. Na koniec dla porównania czas działania programów z O0-3 i Ofast, który pokaże na którym poziomie zachodzi optymalizacja:

Kompilacja i uruchomienie z O0

real    0m0,238s
user    0m0,198s
sys     0m0,041s
9.48683
real    0m0,806s
user    0m0,805s
sys     0m0,000s
Kompilacja i uruchomienie z O1

real    0m0,211s
user    0m0,183s
sys     0m0,028s
9.48683
real    0m0,544s
user    0m0,544s
sys     0m0,000s
Kompilacja i uruchomienie z O2

real    0m0,224s
user    0m0,202s
sys     0m0,022s
9.48683
real    0m0,548s
user    0m0,548s
sys     0m0,000s
Kompilacja i uruchomienie z O3

real    0m0,217s
user    0m0,181s
sys     0m0,035s
9.48683
real    0m0,553s
user    0m0,547s
sys     0m0,000s
Kompilacja i uruchomienie z Ofast

real    0m0,221s
user    0m0,184s
sys     0m0,036s
9.48683
real    0m0,549s
user    0m0,545s
sys     0m0,004s

Do poczytania co jaka opcja robi https://gcc.gnu.org/onlinedocs/gcc/Optimize-Options.html

edit:
@Wibowit słusznie zauważył, że trzeba by jeszcze sprawdzić ile zajmie uruchomienie pustej klasy:

public class Empty{
  public static void main(String[] args){}
}

Wyniki:

real    0m0,059s
user    0m0,084s
sys     0m0,000s
edytowany 2x, ostatnio: Koziołek, 2020-01-13 13:35
Pokaż pozostałe 2 komentarze
@infantylny: to jest tylko preludium, do wstępu, do skróconego opisu. - Koziołek 2020-01-13 13:07
Brakuje jeszcze odpalenia JVMki z pustym programem by zmierzyć czas jej inicjalizacji przed interpretowaniem/ JITowaniem/ etc kodu programu. - Wibowit 2020-01-13 13:15
Dzięki. No to teraz czasy w zasadzie się zrównują jak się odejmie inicjalizację JVMki. - Wibowit 2020-01-13 13:55
Tak, bo to jest zadanie CPU bound, w którym masz tylko pojedyncze alokacje. To będzie szybkie nawet w php. - Koziołek 2020-01-13 13:56

Pozostało 580 znaków

Odpowiedz
Liczba odpowiedzi na stronę

1 użytkowników online, w tym zalogowanych: 0, gości: 1, botów: 0