A zresztą, nie wychodzę dzisiaj z domu, kogo obchodzi że jest piątek.
Czas obecnie: 940 ms
(Liczony ofc na moim komputerze / środowisku etc - u innych efekt może być zupełnie inny)
Cel - zbić czas do połowy tego, tzn. 470 ms
public void Train(double[] inputs, double[] targets)
{
activationResults[inputsIndex] = inputs;
ComputeOutputs();
ComputeErrors(targets);
int jMax, iMax, kMax = network.Length; // <<<<<<<<<<<<
for (int k = 0; k < kMax; ++k)
{
iMax = network[k].Length;
for (int i = 0; i < iMax ; ++i)
{
jMax = network[k][i].Length;
for (int j = 0; j < jMax; ++j)
{
network[k][i][j] += learningCoefficient * errors[k][i] * activationResults[k][j];
}
}
}
}
Tak /nie/ robimy - to, domyślam się, miała być optymalizacja, ale niestety - kompletnie nietrafiona (dlatego nie warto optymalizować na ślepo ;) ).
Jak już wspominałem, zasady dot. ABCE w .NET (tzn. obecne szczegóły implementacyjne które mogą się zmienić blablabla) są bardzo dziwne.
Na przykład dla
for (int i = 0; i < arr.Length; i++)
otrzymujesz ABCE, ale dla
int l = arr.Length;
for (int i = 0; i < l; i++)
Już nie. Cóż, musimy z tym żyć.
Moja wersja:
public void Train(double[] inputs, double[] targets)
{
activationResults[inputsIndex] = inputs;
ComputeOutputs();
ComputeErrors(targets);
for (int k = 0; k < network.Length; ++k)
{
var layer = network[k];
for (int i = 0; i < layer.Length; ++i)
{
var neuron = layer[i];
for (int j = 0; j < neuron.Length; ++j)
{
neuron[j] += learningCoefficient * errors[k][i] * activationResults[k][j];
}
}
}
}
U mnie przyśpieszenie ze średnio 940 do 900 - ok, zawsze jakiś początek.
Oczywiście problem jest taki, że errors i activationResults dalej powodują ABC...
Czas obecnie: 900 ms
Można iść dalej w tym kierunku:
public void Train(double[] inputs, double[] targets)
{
activationResults[inputsIndex] = inputs;
ComputeOutputs();
ComputeErrors(targets);
for (int k = 0; k < network.Length; ++k)
{
var layer = network[k];
var layer_errs = errors[k];
var layer_activ = activationResults[k];
for (int i = 0; i < layer.Length; ++i)
{
var neuron = layer[i];
var error = layer_errs[i];
for (int j = 0; j < neuron.Length; ++j)
{
neuron[j] += learningCoefficient * error * layer_activ[j];
}
}
}
}
Ale wszystkich ABC nie wyeliminujemy. Rozwiązaniem byłoby napisanie kodu... bardziej obiektowo - tzn. rezygnacja z tablic na rzecz większego skupienia się na strukturach i przynależności informacji do nich. Czy to by faktycznie przyśpieszyło program, i jeśli tak to o ile - nie wiem, ale na pewno w tym przypadku pozwoliłoby na eliminację wszystkich checków granic tablicy
.
A, czas dalej mocno skacze, ale ta wersja średnio wykonuje się jakieś 870 (zarejestrowane od 830 do 900)
Czas obecnie: 870 ms
Poprawienie ComputeOutputs w tym samym stylu:
private void ComputeOutputs()
{
for (int k = 0; k < network.Length; ++k)
{
var layer = network[k];
var layer_activ = activationResults[k];
var next_layer_activ = activationResults[k + 1];
for (int i = 0; i < layer.Length; ++i)
{
var activRes = 0.0;
var neuron = layer[i];
for (int j = 0; j < neuron.Length; ++j)
{
activRes += neuron[j] * layer_activ[j];
}
next_layer_activ[i] = neuron.Activate(next_layer_activ[i]);
}
}
}
Czasy z 10 powtórzeń: 882, 796, 846, 830. Średnia około 840 - znowu lepiej.
Czas obecnie: 840 ms
Zostało jeszcze ComputeErrors
.
Po poprawie, czasy - 620, 606, 616, 626, średnia koło 620 0 już lepiej (ja się pytam, tyle tych 6 to gdzie 666?)!
Czas obecnie: 620 ms
Kod:
private void ComputeErrors(double[] targets)
{
var rmsError = 0.0;
for (int k = lastLayerIndex; k >= 0; --k)
{
int kk = k + 1;
var layer = network[k];
var next_layer = k == lastLayerIndex ? null : network[k + 1];
var layer_errs = errors[k];
var next_layer_errs = k == lastLayerIndex ? null : errors[k + 1];
var next_layer_activ = activationResults[k + 1];
for (int i = 0; i < layer.Length; ++i)
{
if (k == lastLayerIndex)
{
layer_errs[i] = targets[i] - next_layer_activ[i];
rmsError += layer_errs[i] * layer_errs[i];
}
else
{
errors[k][i] = 0.0;
for (int j = 0; j < next_layer.Length; ++j)
{
layer_errs[i] += next_layer_errs[j] * next_layer[j][i];
}
}
layer_errs[i] = layer[i].ComputeDerivative(next_layer_activ[i]) * layer_errs[i];
}
}
RMSError = Math.Sqrt(rmsError);
}
Jeśli porównanie z C++, ma być uczciwe, wyrzuciłem też wszystkie występienia IActivatora - czyli było:
public double Activate(double net)
{
return activator.Activate(net);
}
public double ComputeDerivative(double value)
{
return activator.ComputeDerivative(value);
}
Jest:
public double Activate(double net)
{
return 1.0 / (1.0 + Math.Exp(-net));
}
public double ComputeDerivative(double value)
{
return value * (1.0 - value);
}
Czasy: 578, 587, 580, 576, średnia: 570
Czas obecnie: 570 ms
Zdecydowanie najwięcej czasu schodziło w metodzie ComputeOutputs (prawie 40% całej nauki).
Dlatego jej i neuronowi poświęciłem więcej uwagi. I co się okazało - coś słabo idzie optymalizacja indekserów :<. Dlatego zmieniłem trochę klasę neuron (i wyrzuciłem nieużywaną tablicę booli - była potrzebna?) - ostatecznie wygląda tak:
class Neuron
{
double[] weights; // hack?
public Neuron(int weightCount)
{
CreateTables(weightCount + 1);
}
private void CreateTables(int size)
{
weights = new double[size];
}
public double Activate(double net)
{
return 1.0 / (1.0 + Math.Exp(-net));
}
public double ComputeDerivative(double value)
{
return value * (1.0 - value);
}
public double[] Weights { get { return weights; } }
}
Trochę zmian pojawia się w reszcie kodu, ale obecnie czasy to 510, 531, 486, 518 - średnio koło 520. Jeszcze tylko trochę...
Czas obecnie: 520 ms
Zacząłem od zinlinowania funkcji Activate (/i tak/ nie są razem z Computederivative zbyt object-oriented - równie dobrze mogłyby być statyczne, a wygląda na to że JIT (co znowu JIT? Są krótkie, nie mają for-ów itp - może to był tylko problem u mnie) ich nie inlinował).
next_layer_activ[i] = 1.0 / (1.0 + Math.Exp(activRes));
Czas zmniejszył się o parę ms, do około 510. Ciągle za dużo.
No i /the final hack/.
Naprawdę, nie było nic innego co by się narzucało.
Po prostu, tylko jedno miejsce gdzie się coś faktycznie dzieje w tym momencie w kodzie.
To nie moja wina :(
Math.Exp(activRes);
Aż mi wstyd jak na to patrzę.
next_layer_activ[i] = precomputedActiv[(int)(activRes * 100 + 500)];
Zabijcie mnie, ale to działa.
Czasy: 345, 346, 344, 344. Średnia - 344 ms.
Czas obecnie: 344 ms
Czy da się lepiej? Tak. Pozostaje pytanie czy warto, i jak będzie wyglądał kod po tych wszystkich poprawkach. ;) Mi na razie wystarczy, może się znajdzie ktoś kto podejmie wyzwanie i jeszcze znacznie zmniejszy czas wykonania (procentowo, uruchomić na szybszym komputerze to żadna sztuka ;) ). Zawsze też można zrównoleglić...
===========================================================================
Koniec postu, można już kończyć przewijać.
No więc, na moim komputerze, udało się osiągnąć cel z 940 do 470 ms, a nawet go dość znacząco przebić z wynikiem 344 ms.
Wrzucam kod, niech inni też się nim pobawią (uprzedzam że jest teraz brzydszy, więc trzeba się zastanowić na ile ważna jest wydajność a na ile czytelność...).
Dorzucam też snapshoty z EQUATEC, nie wiem czy się komuś przydadzą ale zawsze można zobaczyć jak zmieniał się czas wykonania.
lynx.zip
PS. Tak... Znowu okazuje się że mikrooptymalizacje w C# polegają głównie na omijaniu Tego Wrednego ABC (wrednego jeśli chodzi o takie właśnie optymalizowanie ile się da - w aplikacjach praktycznych
(tzn. biznesowych) ma praktycznie żadne znaczenie dla wydajności (dominowane np. przez SQL) a pozwala znacznie przyśpieszyć debugowanie (w porównaniu do np. wskaźników). Cóż, coś za coś. Ale dlaczego nie da się jej wyłączyć :< ) i pilnowania żeby JIT inlinował to, co ma inlinować.
Takie życie, trzeba się z tym pogodzić.
PPS. Mogłem (i to nawet całkiem prawdopodobne) coś zepsuć podczas optymalizacji, podczas podmieniania tablic na lokalne tablice tymczasowe itp. Nie wiem jakie powinno być prawidłowe wyjście, więc nie mogłem sprawdzić czy się za bardzo nie zmienia. W każdym razie, nie wpływa to w żadnym stopniu na wydajność, bo ilość operacji jest tak czy inaczej taka sama - po prostu trzeba poprawić.
PPPS. Widzę że przez cały post rzucałem tajnymi (dwoma) terminami, takimi jak ABC (przedszkolaka) albo ABCE.
Chodzi o Array Bounds Check, oraz jego Eliminację.
Zasadniczo kiedy mamy kod jak
int i = tab[ndx];
albo
tab[ndx] = 2;
CLR zawsze sprawdza czy ndx na pewno nie jest poza tablicą - w uproszczeniu, wygląda to tak jakby zamiast powyższego kodu było:
if (ndx < 0 || ndx >= tab.Length) { throw new ...; }
int i = tab[ndx];
To działanie to właśnie ABC.
Takie sprawdzenie jest bardzo szybkie, mimo wszystko zajmuję trochę cennego czasu jeśli jest wykonywane bardzo wiele razy (jak teraz).
Ale czasami wiadomo że indeks nie może wyjść poza tablicę. Wtedy JIT może wyeliminować sprawdzenie, czyli mamy ABCE.
Pytanie, kiedy JIT wie że nie możemy wyjść poza tablicę i jak sprytny w tym jest.
Na przykład
for (int i = 0; i < arr.Length; i++) {
arr[i] = 0;
}
teraz wie, a
int l = arr.Length;
for (int i = 0; i < l; i++) {
arr[i] = 0;
}
teraz nie wie.
Te zasady są dość magiczne i zmieniają się (prawdopodobnie) co wersję frameworka, więc nigdy nie wiadomo kiedy eliminacja się wykona a kiedy nie :(. Dlatego, kiedy zależy nam na wydajności, możemy wspomóc JIT sprowadzając co sie da do takiej postaci żeby mu było prosciej.
To takie bardzo skrócone wprowadzenie, dla zainteresowanych np.:
http://blogs.msdn.com/b/clrcodegeneration/archive/2009/08/13/array-bounds-check-elimination-in-the-clr.aspx
PPPPS. O, już ponad dwie godziny minęły?