Wątki, wydajność i wielkie tablice

0

Witam.
Chciałem wykonać test wydajności kilku elementów mojego programu.
Sytuacja wygląda tak:

  • Mamy tablicę zawierającą liczby typu int. Tablica zawiera 150000000 elementów.
  • Tablicę przeglądamy 30 razy pod rząd. (jest to przykład testowy i nie ma biznesowego sensu. Chodzi o sprawdzenie wydajności.)
  • Podczas pobierania danych z tablicy wykonujemy na nich proste operacje matematyczne. Wzór przykładowej operacji:
Math.Truncate((double)(factor * (a[i] - 128) + 128));

gdzie "factor" ma wartość: 2.96952552013 i jest typu double.
a[i] to oczywiście element naszej wielkiej tablicy pobierany sekwencyjnie w pętli.

Na moim procesorze wyniki wyglądają tak:

128 watkow : 146.296090 sekund
8 watkow : 67.5066023 sekund
4 wątki : 71.3351512 sekund
1 watek : 56.9286375 sekund
Bez wątków : 56.9447681 sekund

Jak widać najlepsza wydajność była bez wątków. Bardzo mnie to zaskoczyło. Wątki wykorzystane z tych testach to zwykłe Thread.

Poniżej wykonałem analogiczny test dla wątków typu Task.

128 wątków taska : 74.7024587
1 wątek taska : 57.1611088 sekund
4 wątki taska : 68.196011 sekund
8 wątków taska : 67.284446199999991 sekund

Tutaj sytuacja wygląda podobnie.

W związku z tym mam pytanie.
Dlaczego użycie wątków spowolniło operacje na tablicy?
Spodziewałem się raczej, że wątki (szczególnie 8, gdyż mam 8 rdzeni) spowodują najwyższą wydajność.

Kod:

        private static int[] a = new int[150000000];
        private static int factor = 2.96952552013;
        private static double value = 0;
        private static int iloscWatkow = 8;

        private static void Test()
        {
            TimeSpan globalTime = TimeSpan.Zero;
            Task[] threads = new Task[iloscWatkow];
                
            for (int i = 0; i < 30; i++)
            {
                Stopwatch watch = new Stopwatch();
                watch.Start();
               
                for (int t = 0; t < iloscWatkow; t++)
                {
                    threads[t] = new Task(delegate { ThreadMethod(t); });
                    threads[t].Start();    
                }

                do
                {
                } while (threads.Any(x => x.IsCompleted == false));
                
                watch.Stop();

                globalTime = globalTime.Add(watch.Elapsed);
            }
        }

        private static void ThreadMethod(int start)
        {
            for (int i = start; i < a.Length; i += iloscWatkow)
            {
                value = Math.Truncate((double)(factor * (a[i] - 128) + 128));
            }
        }

Jak widzicie wątki nie operują wewnątrz pętli wykonującej kolejne operacje.
Działają na zasadzie: każdy wątek ma swoją pulę danych w tablicy do przeanalizowania. Stąd parametr Start i skoki w pętlach.

Czy może mi ktoś wyjaśnić dlaczego wątki działają dłużej niż brak wątków lub jeden wątek?

Pozdrawiam.

1

zapewne ma duzo wspolnego ze, skaczesz po tej tablicy i kompilator nie potrafi sobie to dobrze zomptylizowac. Szczegolnie jezeli chodzi o cache alligning

http://stackoverflow.com/questions/12390468/multithreading-slower-than-singlethreading

rozbij to na N tablic (dla N watkow). Dla kazdego watku osobna tablica. Powinno byc lepiej

2

A tak w ogóle to do wykonywania tych samych operacji na ciągu danych to najlepiej stosować Parallel.For i Parallel.ForEach, najprościej i największa szansa na najszybszy rezultat

0

Dziękuję za pomoc. Podzielenie tabeli na części pomogło i to znacznie.

Parallel.For i Parallel.ForEach sprawdzałem przed napisaniem pierwszego posta i wydajność była nieco gorsza od task, ale nieco lepsza od thread.

3

Rdzeni masz wiele, ale pamięć jedną, kontroler pamięci jeden, szynę systemową jedną. Nie ma możliwości, żeby kilka wątków jednocześnie korzystało z pamięci RAM (cache L1, L2 i L3 to inna sprawa). Ponieważ danych jest bardzo dużo to nie ma możliwości, żeby zmieściły się w którejś z pamięci cache procesora, tym bardziej że z każdej komórki korzystasz bardzo krótko. Wątki muszą na siebie czekać i powstają opóźnienia. Ot, uroki nowoczesnych rozwiązań, gdzie procesor jest rzędy wielkości szybszy od pamięci i żaden prefetching czy predykcja skoków mu nie pomogą.
Jeśli tablicę musisz przeglądać wielokrotnie, to rób to kawałkami, czyli wiele razy pierwsze kilkaset kB, potem kolejne itp, pomożesz kontrolerowi pamięci upchnąć dane do L3, a może nawet do L2.

0

Może spróbuj coś z Task.Factory

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