Po pierwsze, większość maszyn JVM zawiera kompilator, więc „interpretowany kod bajtowy” jest w rzeczywistości dość rzadki (przynajmniej w kodzie testowym - nie jest tak rzadki w prawdziwym życiu, w którym Twój kod to zwykle więcej niż kilka trywialnych pętli, które bardzo często się powtarzają ).
Po drugie, spora liczba odnośnych testów porównawczych wydaje się dość tendencyjna (czy to z intencji, czy z niekompetencji, naprawdę nie mogę powiedzieć). Na przykład przed laty patrzyłem na część kodu źródłowego połączonego z jednym z opublikowanych linków. Miał taki kod:
init0 = (int*)calloc(max_x,sizeof(int));
init1 = (int*)calloc(max_x,sizeof(int));
init2 = (int*)calloc(max_x,sizeof(int));
for (x=0; x<max_x; x++) {
init2[x] = 0;
init1[x] = 0;
init0[x] = 0;
}
Ponieważ calloc
zapewnia pamięć, która jest już wyzerowana, for
ponowne użycie pętli do zera jest oczywiście bezużyteczne. Następnie (jeśli pamięć służy) wypełnianie pamięci i tak innymi danymi (i brak zależności od wyzerowania), więc i tak całe zerowanie było całkowicie niepotrzebne. Zastąpienie powyższego kodu prostą malloc
(jak każda rozsądna osoba na początku) poprawiła szybkość wersji C ++ na tyle, aby pokonać wersję Java (o dość szeroki margines, jeśli pamięć służy).
Rozważ (na inny przykład) methcall
test porównawczy użyty we wpisie na blogu w ostatnim linku. Pomimo nazwy (i tego, jak mogłoby to nawet wyglądać), wersja tego C ++ tak naprawdę wcale nie mierzy dużo narzutu wywołania metody. Część kodu, która okazuje się być krytyczna, znajduje się w klasie Toggle:
class Toggle {
public:
Toggle(bool start_state) : state(start_state) { }
virtual ~Toggle() { }
bool value() {
return(state);
}
virtual Toggle& activate() {
state = !state;
return(*this);
}
bool state;
};
Kluczową częścią okazuje się state = !state;
. Zastanów się, co się stanie, gdy zmienimy kod na kodowanie stanu int
zamiast bool
:
class Toggle {
enum names{ bfalse = -1, btrue = 1};
const static names values[2];
int state;
public:
Toggle(bool start_state) : state(values[start_state])
{ }
virtual ~Toggle() { }
bool value() { return state==btrue; }
virtual Toggle& activate() {
state = -state;
return(*this);
}
};
Ta niewielka zmiana poprawia ogólną prędkość o około 5: 1 margines . Mimo, że benchmark był przeznaczony do pomiaru czasu metoda połączenia, w rzeczywistości większość tego, co było pomiaru był czas na konwersję pomiędzy int
i bool
. Z pewnością zgodziłbym się z tym, że nieefektywność wykazana przez oryginał jest niefortunna - ale biorąc pod uwagę, jak rzadko wydaje się występować w prawdziwym kodzie, oraz łatwość, z jaką można to naprawić, kiedy / jeśli się pojawi, trudno jest mi myśleć tego, co wiele znaczy.
W przypadku, gdy ktoś zdecyduje się ponownie uruchomić odnośne testy porównawcze, powinienem również dodać, że istnieje prawie równie trywialna modyfikacja wersji Java, która produkuje (lub przynajmniej raz wyprodukowała - nie uruchomiłem ponownie testów z ostatnie JVM, aby potwierdzić, że nadal tak robią), dość znacząca poprawa również w wersji Java. Wersja Java ma NthToggle :: Activate (), która wygląda następująco:
public Toggle activate() {
this.counter += 1;
if (this.counter >= this.count_max) {
this.state = !this.state;
this.counter = 0;
}
return(this);
}
Zmiana tej opcji na wywołanie funkcji podstawowej zamiast this.state
bezpośredniego manipulowania daje dość znaczną poprawę prędkości (choć nie wystarcza, aby nadążyć za zmodyfikowaną wersją C ++).
W efekcie powstaje fałszywe założenie dotyczące interpretowanych kodów bajtów w porównaniu do niektórych najgorszych testów porównawczych (jakie kiedykolwiek widziałem). Ani też nie daje znaczącego wyniku.
Moje własne doświadczenie jest takie, że przy równie doświadczonych programistach, którzy zwracają jednakową uwagę na optymalizację, C ++ będzie częściej bić Javę - ale (przynajmniej między tymi dwoma) język rzadko robi tak dużą różnicę, jak programiści i projekt. Cytowane testy porównawcze mówią nam więcej o (nie) kompetencjach / (nie) uczciwości ich autorów niż o językach, które zamierzają przeprowadzać.
[Edycja: Jak sugerowano w jednym miejscu powyżej, ale nigdy nie podano tak bezpośrednio, jak prawdopodobnie powinienem, cytuję wyniki, które otrzymałem, kiedy testowałem to ~ 5 lat temu, używając implementacji C ++ i Java, które były aktualne w tym czasie . Nie uruchomiłem ponownie testów z bieżącymi implementacjami. Rzut oka wskazuje jednak, że kod nie został naprawiony, więc wszystko, co by się zmieniło, to zdolność kompilatora do ukrywania problemów w kodzie.]
Jeśli jednak zignorujemy przykłady Java, w rzeczywistości jest możliwe, że interpretowany kod działa szybciej niż kod skompilowany (choć trudny i nieco nietypowy).
Zwykle dzieje się tak, gdy interpretowany kod jest znacznie bardziej zwarty niż kod maszynowy lub działa na procesorze, który ma większą pamięć podręczną danych niż pamięć podręczna kodu.
W takim przypadku mały interpreter (np. Wewnętrzny interpreter implementacji Fortha) może całkowicie zmieścić się w pamięci podręcznej kodu, a program, który interpretuje, mieści się całkowicie w pamięci podręcznej danych. Pamięć podręczna jest zwykle szybsza niż pamięć główna dziesięciokrotnie, a często znacznie więcej (współczynnik 100 nie jest już szczególnie rzadki).
Jeśli więc pamięć podręczna jest szybsza niż pamięć główna o współczynnik N, a do wdrożenia każdego kodu bajtu potrzeba mniej niż N instrukcji kodu maszynowego, kod bajtów powinien wygrać (upraszczam, ale myślę, że ogólna idea powinna nadal być widocznym).