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