Optymalizacja filtrów FIR

0

Chciałbym zrobić implementację filtracji FIR do plików WAV, które mogą zawierać dźwięk, ale też mogą symulować sygnał szerokopasmowy (np. sygnał próbkowany z częstotliwością 22050000Hz może być zapisany w pliku z próbkowaniem 44100Hz, przy czym w odtwarzaczach audio taki plik będzie odtwarzany w 500-krotnie zwolnionym tempie).

Zauważyłem, że filtracja trwa długo i próbuję ją zoptymalizować czasowo. Algorytm jest oparty o bufor pierścieniowy, dla filtrów symetrycznych (na przykład https://en.wikipedia.org/wiki/Sinc_filter ) w rzeczywistości czynność wykonuje się na połowie okna, przy czym każdy współczynnik za wyjątkiem zerowego jest wykorzystywany dwa razy (zerowy jest używany jeden raz).

Czy coś jeszcze da się zoptymalizować czasowo?

Plik testowy maja ok. 500MB, jest to jednokanałowy WAV zawierający ok. 300 milionów próbek 16-bitowych. Jak się zakomentuje kod od komentarza START, do komentarza STOP, to kod trwa ok. 10 sekund. Jest to do zaakceptowania, więc chciałbym się skupić na optymalizacji samej filtracji. Dla tego przykładu trwa ponad 10 minut dla filtru zawierającego 500 współczynników, co daje rzeczywisty filtr mający okno o długości 1001 współczynników.

Można interpretować plik testowy jako sygnał o próbkowaniu 22 mln próbek na sekundę, jak się "rozciągnie" plik źródłowy 500 razy. Po takim rozciągnięciu trzeba zrobić filtr dolno przepustowy, bardzo wąskie filtry przepustowe wymagają dużej liczby współczynników w celu dokładnej filtracji.

Niezaprzeczalnym dowodem na to, że da się filtrować takie sygnały komputerowo w czasie rzeczywistym jest oprogramowanie do odbiorników radiowych SDR, które oprócz samej filtracji wykonują wiele innych czynności z sygnałem o częstotliwości próbkowania liczonej w megahercach i to wszystko w czasie rzeczywistym. W takim razie musi się dać to jeszcze bardziej zoptymalizować. Nie chodzi o optymalizację samego filtra (zmniejszenie liczby współczynników kosztem dokładności), ale o optymalizację filtrowania zadanym filtrem FIR. Interesuje mnie optymalizacja kodu pomiędzy komentarzami START i STOP.

Poniżej kod procedury dla filtrów symetrycznych, okno filtru jest w tablicy FilterWindow typu llong[], w której na pozycji 0 jest współczynnik środkowy, a na kolejnych pozycjach są współczynniki kolejno dalsze od środka. Innymi słowy, ta tablica zawiera połowę okna filtru wraz z elementem środkowym.

void AppCore::ProcessPassFilter(string FileI, string FileO)
{
    // Rzeczywista długość filtru dla danej liczby wspolczynnikow
    // W tym przypadku kazdy filtr ma okno symetryczne o nieparzystej liczbie probek
    // FilterSize to liczba wspolczynnikow okna filtru nie liczac wspolczynnika zerowego
    int FilterWindowWorkSize = (FilterSize * 2) + 1;

    // Zerowanie bufora roboczego, ktory jest typu short[] i bedzie zawierac
    // dokladnie tyle ostatnich probek z pliku zrodlowego, jak dlugi jest filtr
    for (int I = 0; I < FilterWindowWorkSize; I++)
    {
        FilterWindowWork[I] = 0;
    }

    // Przygotowanie plikow, DataI to plik zrodlowy, DataO to plik docelowy
    EdenClass::WaveFile DataI;
    EdenClass::WaveFile DataO;
    DataI.FileOpen(FileI, true);
    DataO.FileNew(FileO, DataI.SampleRate, 1, 0);

    // Dlugosc pliku zrodlowego
    llong FileSize = DataI.Length;

    // Bufor do odczytu i zapisu danych, ChunkSize jest typu llong
    // i ma wartosc 1024, ustawiana przed wywolanie procedury
    short *Buf = new short[ChunkSize];

    // Miejsce odczytu i zapisu w pliku
    llong FileOffset = 0;

    // Wskaznik miejsca w buforze roboczym wskazujacy miejsce zapisu odczytanych probek
    int WindowPointer = 0;

    // Wskazniki miejsc w buforze roboczym wskazujace miejsca odczytu probek przy filtracji
    int WindowPointer1 = 0;
    int WindowPointer2 = 0;

    // Petla glowna
    while (ChunkSize > 0)
    {
        // Odczyt porcji danych z pliku zrodlowego
        DataI.ReadM(Buf, FileOffset, ChunkSize);

        // START

        // Przetwarzanie odczytanej porcji danych, jedna iteracja to jedna probka
        for (int II = 0; II < ChunkSize; II++)
        {
            Zapisanie probki do bufora roboczego
            FilterWindowWork[WindowPointer] = Buf[II];

            // Ustawianie wskaznikow odczytu probek do filtru na miejsce srodkowe miedzy pierwsza a ostatnia odczytana probka
            WindowPointer1 = WindowPointer - FilterSize - 0;
            if (WindowPointer1 < 0) { WindowPointer1 += FilterWindowWorkSize; }
            WindowPointer2 = WindowPointer1;

            // Obliczenie wartosci probki na podstawie wspolczynnika zerowego (znajdujacego sie w osi symetrii okna filtru)
            llong Temp = FilterWindowWork[WindowPointer1];
            llong XXX = (Temp * FilterWindow[0]);

            // Przesuniecie wskaznikow odczytu o jedna probke, jeden do przodu, drugi do tylu,
            // ale z zachowanie pierscieniowego charakteru bufora
            WindowPointer1++;
            if (WindowPointer1 == FilterWindowWorkSize) { WindowPointer1 = 0; }
            if (WindowPointer2 == 0) { WindowPointer2 = FilterWindowWorkSize; }
            WindowPointer2--;

            // Uwzglednianie pozostalych wspolczynnikow filtru
            for (int III = 1; III < FilterSize; III++)
            {
                // Obliczanie skladowych wartosci probki dla pozostalych wspolczynnikow
                // i dodawanie obliczonych wartosci do wartosci na podstawie wspolczynnika zerowego
                Temp = FilterWindowWork[WindowPointer1];
                XXX = XXX + (Temp * FilterWindow[III]);
                Temp = FilterWindowWork[WindowPointer2];
                XXX = XXX + (Temp * FilterWindow[III]);

                // Przesuniecie wskaznikow odczytu o jedna probke, jeden do przodu, drugi do tylu,
                // ale z zachowanie pierscieniowego charakteru bufora
                WindowPointer1++;
                if (WindowPointer1 == FilterWindowWorkSize) { WindowPointer1 = 0; }
                if (WindowPointer2 == 0) { WindowPointer2 = FilterWindowWorkSize; }
                WindowPointer2--;
            }

            // WindowAmplitude to wartosc stala 65536
            XXX = XXX / (WindowAmplitude);

            // Ograniczanie wartosci obliczonej probki w celu unikniecia bledu z powodu zaokraglen
            if (XXX > 32767) { XXX = 32767; }
            if (XXX < -32768) { XXX = -32768; }

            // Zapisanie obliczonej probki do bufora do zapisu danych
            Buf[II] = XXX;

            // Przesuniecie wskaznika zapisu do bufora roboczego o jedna probke do przodu
            // z zachowaniem pierscieniowego charakteru bufora
            WindowPointer++;
            if (WindowPointer >= FilterWindowWorkSize) { WindowPointer = 0; }
        }

        // STOP

        // Zapis porcji danych do pliku docelowego
        DataO.WriteM(Buf, FileOffset, ChunkSize);

        // Przesuwanie miejsca w pliku o dlugosc ostatniej porcji danych
        FileOffset += ChunkSize;

        // Ostatnia porcja danych moze byc krotsza w przypadku, gdy dlugosc pliku
        // nie jest podzielna przez dlugosc porcji, a po przetworzeniu ostatniej porcji
        // jej nowa wielkosc wynosi 0 i to powoduje zakonczenie petli
        if ((FileOffset + ChunkSize) > FileSize)
        {
            ChunkSize = FileSize - FileOffset;
        }
    }

    // Niszczenie bufora do odczytu i zapisu danych
    delete[] Buf;

    // Zamkniecie plikow
    DataI.FileClose();
    DataO.FileClose();
}

Poniżej kod procedury dla dowolnego filtru FIR, w komentarzach są opisane tylko różnice w stosunku do powyższej procedury.
W tym przypadku, tablica FilterWindow zawiera całe okno filtru.

void AppCore::ProcessUserFilter(string FileI, string FileO)
{
    // W tym przypadku FilterSize to rzeczywista dlugosc okna filtru
    for (int I = 0; I < FilterSize; I++)
    {
        FilterWindowWork[I] = 0;
    }

    EdenClass::WaveFile DataI;
    EdenClass::WaveFile DataO;
    DataI.FileOpen(FileI, true);
    DataO.FileNew(FileO, DataI.SampleRate, 1, 0);

    llong FileSize = DataI.Length;
    short *Buf = new short[ChunkSize];

    llong FileOffset = 0;
    int WindowPointer = 0;

    // Jest tylko jeden wskaznik odczytu z bufora roboczego
    int WindowPointer1 = 0;

    ProgressAll = FileSize / ChunkSize;
    while (ChunkSize > 0)
    {
        DataI.ReadM(Buf, FileOffset, ChunkSize);

        // START

        for (int II = 0; II < ChunkSize; II++)
        {
            // Zapisanie probki do bufora roboczego
            FilterWindowWork[WindowPointer] = Buf[II];

            // Ustawianie wskaznika odczytu probek do filtru na miejsce za ostatniej odczytana probka,
            // bedzie wskazywac wartosc pierwszej (najstarszej) odczytanej probki
            WindowPointer1 = WindowPointer - FilterSize - 0;
            if (WindowPointer1 < 0) { WindowPointer1 += FilterSize; }

            llong Temp = 0;
            llong XXX = 0;

            if (WindowPointer1 == FilterSize) { WindowPointer1 = 0; }
            for (int III = 0; III < FilterSize; III++)
            {
                // Obliczanie skladowych wartosci probki dla wszystkich wspolczynnikow
                // i dodawanie obliczonych wartosci do wartosci na podstawie wspolczynnika zerowego
                Temp = FilterWindowWork[WindowPointer1];
                XXX = XXX + (Temp * FilterWindow[III]);

                WindowPointer1++;
                if (WindowPointer1 == FilterSize) { WindowPointer1 = 0; }
            }


            XXX = XXX / (WindowAmplitude);
            if (XXX > 32767) { XXX = 32767; }
            if (XXX < -32768) { XXX = -32768; }
            Buf[II] = XXX;

            WindowPointer++;
            if (WindowPointer >= FilterSize) { WindowPointer = 0; }
        }

        // STOP

        DataO.WriteM(Buf, FileOffset, ChunkSize);

        FileOffset += ChunkSize;
        if ((FileOffset + ChunkSize) > FileSize)
        {
            ChunkSize = FileSize - FileOffset;
        }
        ProgressCurrent += ProgressStep;
    }

    delete[] Buf;

    DataI.FileClose();
    DataO.FileClose();
}
0

Czy czas mierzysz na wersji release z włączoną optymalizacją?

       // WindowAmplitude to wartosc stala 65536
        XXX = XXX / (WindowAmplitude);
XXX >>= 16;
        WindowPointer1 = WindowPointer - FilterSize - 0;
        if (WindowPointer1 < 0) { WindowPointer1 += FilterWindowWorkSize; }

Ile wynosi FilterSize i FilterWindowWorkSize? Jeśli są potęgą 2 to te porównania też można zastąpić operacjami logicznymi.

0

Jak na razie, testowałem na Debug, ale przetestuję na Release.

Zamiana dzielenia na przesunięcie bitowe to dobry pomysł.

Natomiast FilterSize i FilterWindowWorkSize mogą przyjmować dowolną wartość od 1 do 5000 (ograniczona wielkością tablic, która jest stała i znana w chwili kompilacji, którą można zmienić, jeżeli stwierdzę taką potrzebę). Spróbuję połączyć oba podejścia i wprowadzić IFa, że jeżeli wartość jest potęgą dwójki (zapamiętane w boolean), to zostanie zastosowana operacja logiczna, w przeciwnym wypadku tak, jak do tej pory.

0

Tak a propos pozbywania się ifów:

if (XXX > 32767) { XXX = 32767; }
if (XXX < -32768) { XXX = -32768; }

można zastąpić tym:

long long v1 = XXX + 32767;
long long v2 = XXX - 32767;

unsigned long long temp = v1 >> 63;     
v1 ^= temp;                   
v1 += temp & 1;               

temp = v2 >> 63;     
v2 ^= temp;                   
v2 += temp & 1;               

XXX = (v1 - v2) >> 1;
0

Zapomniałem napisać, że "llong" to po prostu zdefiniowany typ "long long int" (po prostu tak mi jest wygodniej, użyłem typedef). Dziękuję za wskazówki, jednak drogą eliminacji dotarłem do miejsca, które sprawia największy problem. Poniższy kod wykonuje się przez 25-30 sekund i to nie jest problem, powiedzmy, że stosując Wasze porady skrócę do 15 sekund, ale to są czynności podstawowe. W tym przykładzie akurat jest wyłączony najważniejszy fragment algorytmu znajdujący się w pętli for (int III = 1; III < FilterSize; III++). Przy czasie rzędu 15-20 minut, 10-20 sekund w tą czy w tą przestaje robić różnicę.

void AppCore::ProcessPassFilter(string FileI, string FileO)
{
    int FilterWindowWorkSize = (FilterSize * 2) + 1;

    for (int I = 0; I < FilterWindowWorkSize; I++)
    {
        FilterWindowWork[I] = 0;
    }

    EdenClass::WaveFile DataI;
    EdenClass::WaveFile DataO;
    DataI.FileOpen(FileI, true);
    DataO.FileNew(FileO, DataI.SampleRate, 1, 0);

    llong FileSize = DataI.Length;
    short *Buf = new short[ChunkSize];

    llong FileOffset = 0;
    int WindowPointer = 0;
    int WindowPointer1 = 0;
    int WindowPointer2 = 0;

    ProgressAll = FileSize / ChunkSize;
    while (ChunkSize > 0)
    {
        DataI.ReadM(Buf, FileOffset, ChunkSize);

        for (int II = 0; II < ChunkSize; II++)
        {
            FilterWindowWork[WindowPointer] = Buf[II];

            WindowPointer1 = WindowPointer - FilterSize - 0;
            if (WindowPointer1 < 0) { WindowPointer1 += FilterWindowWorkSize; }

            WindowPointer2 = WindowPointer1;
            llong Temp = FilterWindowWork[WindowPointer1];
            llong XXX = (Temp * FilterWindow[0]);

            WindowPointer1++;
            if (WindowPointer1 == FilterWindowWorkSize) { WindowPointer1 = 0; }
            if (WindowPointer2 == 0) { WindowPointer2 = FilterWindowWorkSize; }
            WindowPointer2--;
            for (int III = 1; III < FilterSize; III++)
            {
                //Temp = FilterWindowWork[WindowPointer1];
                //XXX = XXX + (Temp * FilterWindow[III]);
                //Temp = FilterWindowWork[WindowPointer2];
                //XXX = XXX + (Temp * FilterWindow[III]);

                WindowPointer1++;
                if (WindowPointer1 == FilterWindowWorkSize) { WindowPointer1 = 0; }
                if (WindowPointer2 == 0) { WindowPointer2 = FilterWindowWorkSize; }
                WindowPointer2--;
            }


            XXX = XXX / (WindowAmplitude);
            if (XXX > 32767) { XXX = 32767; }
            if (XXX < -32768) { XXX = -32768; }
            Buf[II] = XXX;

            WindowPointer++;
            if (WindowPointer >= FilterWindowWorkSize) { WindowPointer = 0; }
        }

        DataO.WriteM(Buf, FileOffset, ChunkSize);

        FileOffset += ChunkSize;
        if ((FileOffset + ChunkSize) > FileSize)
        {
            ChunkSize = FileSize - FileOffset;
        }
        ProgressCurrent += ProgressStep;
    }

    delete[] Buf;

    DataI.FileClose();
    DataO.FileClose();
}

Jak uruchomię odczyt wartości, to następuje zmiana typu short (element w tablicy FilterWindowWork) na typ long long int (zmienna Temp). W tym przypadku kod też wykonuje się niecałe 30 sekund, czyli konwersja typu nie jest problemem. Ta zmienna Temp jest tylko dlatego, żeby obliczenia wykonywać na wartościach long long int, żeby nie przekroczyć zakresu, co może objawić się błędami. Przy pisaniu kodu chciałem uniknąć liczb ułamkowych, bo podobno wydajność obliczeń na nich jest niższa. Tablica FilterWindow jest typu long long int dlatego, że oblicza się ją jeden raz, przed uruchomieniem procedury filtracji, a potem się tylko odczytuje wartości. Sam filtr jest liczony na wartościach long double, a potem współczynniki są mnożone przez 65536, a potem następuje konwersja do long long int. Wobec tego, maksymalna wartość, jaka musi się zmieścić w zakresie to 32768 (maksymalna amplituda próbki) pomnożona przez 65536 (maksymalna wartość współczynnika filtru) i jeszcze pomnożona przez liczbę współczynników filtru.

            for (int III = 1; III < FilterSize; III++)
            {
                Temp = FilterWindowWork[WindowPointer1];
                //XXX = XXX + (Temp * FilterWindow[III]);
                Temp = FilterWindowWork[WindowPointer2];
                //XXX = XXX + (Temp * FilterWindow[III]);

                WindowPointer1++;
                if (WindowPointer1 == FilterWindowWorkSize) { WindowPointer1 = 0; }
                if (WindowPointer2 == 0) { WindowPointer2 = FilterWindowWorkSize; }
                WindowPointer2--;
            }

Potem podstawiłem do kodu wartość stałą na próbę i cały kod wykonywał się 7 minut:

            for (int III = 1; III < FilterSize; III++)
            {
                Temp = 12345;
                //Temp = FilterWindowWork[WindowPointer1];
                XXX = XXX + (Temp * FilterWindow[III]);
                //Temp = FilterWindowWork[WindowPointer2];
                XXX = XXX + (Temp * FilterWindow[III]);

                WindowPointer1++;
                if (WindowPointer1 == FilterWindowWorkSize) { WindowPointer1 = 0; }
                if (WindowPointer2 == 0) { WindowPointer2 = FilterWindowWorkSize; }
                WindowPointer2--;
            }

Potem przywróciłem oryginalny kod i działa prawdopodobnie ponad 30 minut (po 10 minutach było jakieś 1/3 i przerwałem pracę). Wygląda na to, że to mnożenie jest problemem, a jest to podstawa filtru FIR. Dziwne jest to, że program działa wielokrotnie dłużej, gdy jedyną różnicą jest źródło jednej liczby (hardkodowana czy odczytana z tablicy), a sam odczyt jest pomijalny.

            for (int III = 1; III < FilterSize; III++)
            {
                Temp = FilterWindowWork[WindowPointer1];
                XXX = XXX + (Temp * FilterWindow[III]);
                Temp = FilterWindowWork[WindowPointer2];
                XXX = XXX + (Temp * FilterWindow[III]);

                WindowPointer1++;
                if (WindowPointer1 == FilterWindowWorkSize) { WindowPointer1 = 0; }
                if (WindowPointer2 == 0) { WindowPointer2 = FilterWindowWorkSize; }
                WindowPointer2--;
            }

Czy jedynym ratunkiem jest próba zamiany typu long long int (64bit) na long int(32bit), czy coś innego da się zrobić? W chwili obecnej, elementy FilterWindowWork może przyjmować dowolne wartości od -32768 do 32768 (pełny zakres typu short), a elementy FilterWindow mogą przyjmować dowolne wartości od 0 do 65536 (górną granicę mogę regulować przez zmianę kilku stałych, ale to wpływa na dokładność filtracji).

0

Przy pisaniu kodu chciałem uniknąć liczb ułamkowych, bo podobno wydajność obliczeń na nich jest niższa.

Wszystkie współczesne DAW pracują na float/double, więc to nie jest prawda.

Czy jedynym ratunkiem jest próba zamiany typu long long int (64bit) na long int(32bit), czy coś innego da się zrobić?

Faktem jest, że dla 32-bitowej aplikacji 64-bitowy typ nie jest jakoś szczególnie optymalny.

0

Zainteresuj się może instrukcjami MMX/SSE. Do tego typu rzeczy służą.

0

Masz możliwość sprawdzić na ile pomoże Ci optymalizacja typu loop unrolling?

0
0x666 napisał(a):

Przy pisaniu kodu chciałem uniknąć liczb ułamkowych, bo podobno wydajność obliczeń na nich jest niższa.

Wszystkie współczesne DAW pracują na float/double, więc to nie jest prawda.

Czy jedynym ratunkiem jest próba zamiany typu long long int (64bit) na long int(32bit), czy coś innego da się zrobić?

Faktem jest, że dla 32-bitowej aplikacji 64-bitowy typ nie jest jakoś szczególnie optymalny.

Czyli rozumiem, że najbardziej rozsądne jest próba przerobienia na double lub ewentualnie int 32bit i przetestowanie.

Azarien napisał(a):

Zainteresuj się może instrukcjami MMX/SSE. Do tego typu rzeczy służą.

Czy przy użyciu tych instrukcji programuje się w języku C/C++ i kompilator sam dobierze instrukcje MMX lub SSE, czy wtedy trzeba pisać wstawkę ASM?

alagner napisał(a):

Masz możliwość sprawdzić na ile pomoże Ci optymalizacja typu loop unrolling?

Mogę spróbować poszukać w parametrach qmake.exe. Jeżeli nie znajdę, to jeżeli wygeneruję kod z rozwiniętą pętlą (tyle zestawów instrukcji, ile jest iteracji pętli przy założeniu, że liczba jest stała i znana w chwili kompilacji) zamiast pętli, to też będzie odpowiednik optymalizacji loop unrolling?

0
andrzejlisek napisał(a):

Czy przy użyciu tych instrukcji programuje się w języku C/C++ i kompilator sam dobierze instrukcje MMX lub SSE

Teoretycznie najnowsze wersje Visual C++ automatycznie używają gdzieniegdzie SSE2 albo AVX (jeśli się włączy) ale nie sprawdzałem na ile jest to skuteczne.

czy wtedy trzeba pisać wstawkę ASM?

Można pisać w asm, a można używać tzw. intrinsics, czyli funkcji które gwarantują użycie danych instrukcji.

0

Czyli rozumiem, że najbardziej rozsądne jest próba przerobienia na double lub ewentualnie int 32bit i przetestowanie.

Najpierw spróbuj z 32-bitami, konwersja na double miałaby sens, gdybyś robił jakieś bardziej zaawansowane obliczenia. Z tego, co pamiętam, konwersja double -> int sporo taktów zabiera. Choć nie oszukujmy się, tego typu filtry są moco-żerne, tutaj jedyna sensowna optymalizacja to po prostu zmniejszenie wielkości filtra.

0
0x666 napisał(a):

Czyli rozumiem, że najbardziej rozsądne jest próba przerobienia na double lub ewentualnie int 32bit i przetestowanie.

Najpierw spróbuj z 32-bitami, konwersja na double miałaby sens, gdybyś robił jakieś bardziej zaawansowane obliczenia. Z tego, co pamiętam, konwersja double -> int sporo taktów zabiera. Choć nie oszukujmy się, tego typu filtry są moco-żerne, tutaj jedyna sensowna optymalizacja to po prostu zmniejszenie wielkości filtra.

W obróbce audio, nawet o jakości studyjnej, może wystarcza inne filtry. Natomiast, jak chodzi o odbiór SDR, to chyba muszą być takie długie, skoro obrabia się sygnał próbkowany z częstotliwością kilku megaherców i trzeba wyłowić bardzo wąski wycinek z widma sygnału (o szerokości na przykład 2-3kHz). A skoro programy do SDR dają rade w czasie rzeczywistym takie coś obrabiać, to musi być na to jakiś sposób.

0

No dobra, ale czy tam wykorzystywane są filtry sinc?

0
0x666 napisał(a):

No dobra, ale czy tam wykorzystywane są filtry sinc?

Tak, wykorzystane są filtry sinc, przy czym jedno filtrowanie może filtrować kilka pasm jednocześnie z różnymi amplitudami, bo filtry sinc można dodawać i odejmować.

0

Przeprowadziłem testy szybkości działania dla różnych typów danych poprzez pomiar czasu. Z poprzednich prób wynika, że największym problemem nie jest obsługa samej pętli (jednakże mogę spróbować rozwinięcia lub kilku kroków przy jednej iteracji), tylko obliczanie sumy wartości i średniej.

Tak wygląda kod do testowania:

void AppCore::ProcessPassFilter(string FileI, string FileO)
{

    QTime Tx;

    int FilterWindowWorkSize = (FilterSize * 2) + 1;

    for (int I = 0; I < FilterWindowWorkSize; I++)
    {
        FilterWindowWork[I] = 0;
    }

    EdenClass::WaveFile DataI;
    EdenClass::WaveFile DataO;
    DataI.FileOpen(FileI, true);
    DataO.FileNew(FileO, DataI.SampleRate, 1, 0);

    llong FileSize = DataI.Length;
    short *Buf = new short[ChunkSize];

    llong FileOffset = 0;
    int WindowPointer = 0;
    int WindowPointer1 = 0;
    int WindowPointer2 = 0;

    #define TypeTest double

    TypeTest FilterWindow_I[2000];
    TypeTest FilterWindowWork_I[2000];
    for (int OO = 0; OO < FilterWindowWorkSize; OO++)
    {
        FilterWindow_I[OO] = FilterWindow[OO];
    }

    Tx.start();

    ProgressAll = FileSize / ChunkSize;

    while (ChunkSize > 0)
    {
        DataI.ReadM(Buf, FileOffset, ChunkSize);

        for (int II = 0; II < ChunkSize; II++)
        {
            FilterWindowWork_I[WindowPointer] = Buf[II];

            WindowPointer1 = WindowPointer - FilterSize - 0;
            if (WindowPointer1 < 0) { WindowPointer1 += FilterWindowWorkSize; }

            WindowPointer2 = WindowPointer1;
            TypeTest Temp = FilterWindowWork_I[WindowPointer1];
            TypeTest XXX = (Temp * FilterWindow_I[0]);

            WindowPointer1++;
            if (WindowPointer1 == FilterWindowWorkSize) { WindowPointer1 = 0; }
            if (WindowPointer2 == 0) { WindowPointer2 = FilterWindowWorkSize; }
            WindowPointer2--;
            for (int III = 1; III < FilterSize; III++)
            {
                Temp = FilterWindowWork_I[WindowPointer1];
                XXX = XXX + (Temp * FilterWindow_I[III]);
                Temp = FilterWindowWork_I[WindowPointer2];
                XXX = XXX + (Temp * FilterWindow_I[III]);

                WindowPointer1++;
                if (WindowPointer1 == FilterWindowWorkSize) { WindowPointer1 = 0; }
                if (WindowPointer2 == 0) { WindowPointer2 = FilterWindowWorkSize; }
                WindowPointer2--;
            }


            XXX = XXX / (WindowAmplitude);
            if (XXX > 32767) { XXX = 32767; }
            if (XXX < -32768) { XXX = -32768; }
            Buf[II] = XXX;

            WindowPointer++;
            if (WindowPointer >= FilterWindowWorkSize) { WindowPointer = 0; }
        }

        DataO.WriteM(Buf, FileOffset, ChunkSize);

        FileOffset += ChunkSize;
        if ((FileOffset + ChunkSize) > FileSize)
        {
            ChunkSize = FileSize - FileOffset;
        }
        ProgressCurrent += ProgressStep;
    }

    int T = Tx.elapsed() / 1000;
    cout << "Czas: " << T << endl;

    delete[] Buf;

    DataI.FileClose();
    DataO.FileClose();
}

Testowane typy podstawiam za słowo TypeTest. Przetestowałem następujące typy i czasy działania procedury od Tx.start() do Tx.elapsed() i wyszły takie czasy:
int - 381
long - 382
llong - 1633
float - 395
double - 749

Int i long na x86 to jest to samo, oba typy to liczba 32-bitowa. Wniosek jest taki, że obliczenia najlepiej wykonywać na danych typu long, jedynie muszę sprawdzić, czy nie ma błędów.

0

Widzę tam jakieś dzielenie przez zmienną. Dzielenie jest potwornie powolne, spróbuj napisać bez, albo dziel przez stałą. Dzielenie przez stałą można zrobić mnożeniem, przesunięciami bitowymi, dodawaniem itp.

Masz też warunki wewnątrz pętli. Na pewno je potrzebujesz? Skok warunkowy to kilkanaście cykli jeśli zostanie źle przewidziany. Dla porównania dodawanie to zwykle jeden lub pół cyklu.

No i oczywiście do takiego działania koniecznie trzeba użyć SSE/AVX. Kompilatory są w takich optymalizacjach zwykle dość słabe, więc pewnie bez ręcznego użycia intrinsiców lub wstawki w asm się nie obejdzie. Ale na start zapoznaj się z opcjami kompilatora, bo domyślnie żaden kompilator nawet nie próbuje. Jeśli to gcc, to zobacz sobie -funroll-loops, -ftree-vectorize, -msse*, -O3.

To się może przydać: https://www.google.pl/url?sa=t&source=web&rct=j&url=http://www.intel.se/content/dam/www/public/us/en/documents/white-papers/fir-filter-sse-instructions-paper.pdf&ved=0ahUKEwiK6ZOOvOPSAhWQKiwKHX4qAoIQFggjMAE&usg=AFQjCNGEWyBHbu9utTOjnq_4k2EvQ9kymg&sig2=aivhcTKlY-6Te-eiKU9S3Q

A, no i jeszcze taka rzecz: w klasycznym analogowym radiu nie stosuje się filtrów o wysokich rzędach, nie stosuje się też filtrów fir, a jakoś daje się wyłowić pasmo o szerokości 0,5 MHz z sygnału o częstotliwości 100 MHz. Nie bardzo więc rozumiem po co Ci tyle współczynników. Zwłaszcza, że analogowe radio w Polsce gra i tak lepiej niż DAB+ (w sensie jakości dźwięku).

Podejrzewam więc, że tę algorytmy działające w czasie rzeczywistym robią dwie rzeczy: używają instrukcji SIMD oraz mają znacznie prostsze filtry.

0

Trochę z innej mańki...
Czy nie efektywniejsze byłoby:
a. przejście w dziedzinę f, czyli:
FFT, mnożenie widma przez transmitancję filtru, IFFT?
b. przemiana częstotliwości w górę/dół, ew. decymacja i zastosowanie n razy prostszego filtru HP/LP zamiast BP? Ew. zastosowania wszystkich innych trików DSPowo-radiotechnicznych, których nazw teraz nie pamiętam? ;)

O zrobieniu tego na FPGA zamiast w sofcie to nie mówię nawet ;)

Pytanie co jest celem: super stromy filtr, odzyskanie konkretnego sygnału zmodulowanego jakoś czy jeszcze co innego.

0
Krolik napisał(a):

Widzę tam jakieś dzielenie przez zmienną. Dzielenie jest potwornie powolne, spróbuj napisać bez, albo dziel przez stałą. Dzielenie przez stałą można zrobić mnożeniem, przesunięciami bitowymi, dodawaniem itp.

Po napisaniu postu wyrzuciłem zmienną Temp i linię XXX = XXX / (WindowAmplitude); zamieniłem na XXX = XXX >> 16; i nie zauważyłem istotnej różnicy, ale oczywiście zostawiam zmienione.

Masz też warunki wewnątrz pętli. Na pewno je potrzebujesz? Skok warunkowy to kilkanaście cykli jeśli zostanie źle przewidziany. Dla porównania dodawanie to zwykle jeden lub pół cyklu.

Czy miałeś na myśli warunki odnośnie zmiennej ChunkSize na końcu głównej pętli, czy odnośnie zmiennych WindowPointer1 i WindowPointer2 w pętli wewnętrznej?

No i oczywiście do takiego działania koniecznie trzeba użyć SSE/AVX. Kompilatory są w takich optymalizacjach zwykle dość słabe, więc pewnie bez ręcznego użycia intrinsiców lub wstawki w asm się nie obejdzie. Ale na start zapoznaj się z opcjami kompilatora, bo domyślnie żaden kompilator nawet nie próbuje. Jeśli to gcc, to zobacz sobie -funroll-loops, -ftree-vectorize, -msse*, -O3.

To się może przydać: https://www.google.pl/url?sa=t&source=web&rct=j&url=http://www.intel.se/content/dam/www/public/us/en/documents/white-papers/fir-filter-sse-instructions-paper.pdf&ved=0ahUKEwiK6ZOOvOPSAhWQKiwKHX4qAoIQFggjMAE&usg=AFQjCNGEWyBHbu9utTOjnq_4k2EvQ9kymg&sig2=aivhcTKlY-6Te-eiKU9S3Q

Kompilator to MinGW32 instalowany razem z QT Creator w stysyemie Windows. Może faktycznie w profesjonalnym oprogramowaniu stosuje się różne wstawki i sztuczki z SSE, AVX i SIMD, mi chodzi o optymalizację na poziomie C++, na ile się da, potem podejmę decyzję, czy czas działania jest akceptowalny, czy wtedy sięgnąć po SSE.

A, no i jeszcze taka rzecz: w klasycznym analogowym radiu nie stosuje się filtrów o wysokich rzędach, nie stosuje się też filtrów fir, a jakoś daje się wyłowić pasmo o szerokości 0,5 MHz z sygnału o częstotliwości 100 MHz. Nie bardzo więc rozumiem po co Ci tyle współczynników. Zwłaszcza, że analogowe radio w Polsce gra i tak lepiej niż DAB+ (w sensie jakości dźwięku).

Podejrzewam więc, że tę algorytmy działające w czasie rzeczywistym robią dwie rzeczy: używają instrukcji SIMD oraz mają znacznie prostsze filtry.

W ostateczności program ma symulować modulację i demodulacje amplitudy i częstotliwości. O ile z samymi algorytmami nie mam żadnych problemów i wykonują się w akceptowalnym czasie, o tyle z filtrami jest problem wydajnościowy. Pierwszą rzeczą jest znaczne zwiększenie częstotliwości próbkowania (od kilkadziesiąt razy do kilkaset razy), im więcej, tym bardziej dokładnie, ale też dłuższy czas pracy. Zgodnie ze starą zasadą, zwiększenie częstotliwości próbkowania X-krotnie polega na powtórzeniu X razy każdej próbki. Potem należy przefiltrować przebieg filtrem dolnoprzepustowym, żeby wyciąć harmoniczne, czyli częstotliwością graniczną filtry jest połowa częstotliwości próbkowania przed zwiększeniem liczby próbek. Mogę ewentualnie obniżyć tą częstotliwość, żeby wyciąć śmieci, bo zbocze odcięcia w filtrze FIR nigdy nie jest idealnie pionowe.

0
alagner napisał(a):

Trochę z innej mańki...
Czy nie efektywniejsze byłoby:
a. przejście w dziedzinę f, czyli:
FFT, mnożenie widma przez transmitancję filtru, IFFT?
b. przemiana częstotliwości w górę/dół, ew. decymacja i zastosowanie n razy prostszego filtru HP/LP zamiast BP? Ew. zastosowania wszystkich innych trików DSPowo-radiotechnicznych, których nazw teraz nie pamiętam? ;)

O zrobieniu tego na FPGA zamiast w sofcie to nie mówię nawet ;)

Pytanie co jest celem: super stromy filtr, odzyskanie konkretnego sygnału zmodulowanego jakoś czy jeszcze co innego.

Głównym celem jest prosty symulator modulacji i demodulacji amplitudy i częstotliwości na własne potrzeby. Z algorytmami modulacji i demodulacji nie mam żadnych problemów, z filtrami FIR jest problem długiego czasu trwania. Znam dobrze ogólne działanie filtra FIR, chciałem dowiedzieć się, jak to się implementuje profesjonalnie i już wiem, że najlepiej wykorzystać instrukcje SSE i inne podobne. Jeżeli się nie da, to zaakceptuję tak, jak jest, bo algorytm działa poprawnie, już wiem, że zmiana wartości 64-bitowych na 32-bitowe znacznie przyspiesza obliczenia. Pozostaje przetestować, czy nie ma błędów i ewentualnie spróbować wyeliminować inne niepotrzebne operacje.

0

No ale czekaj, ja tu trochę problem z założeniami widzę, chyba że projekt ma być czysto edukacyjny. Natomiast dla tak potężnych filtrów sugerowane przeze mnie przejście na domenę częstotliwości jak najbardziej ma sens, tu masz dowód jeśli mi nie wierzysz ;)
www.ece.rice.edu/~srs1/files/Circuits_11_4.pdf

Pzdr.

EDIT: wg wzorów z PDFa w dziedzinie częstotliwości będziesz (dla FIRa o 1001 współczynnikach i 300 mln próbek) jakoś ~140x szybszy. O ile dobrze zaimplementujesz FFT ;)

0

Pozbyłbym się instrukcji warunkowych, które mogą mieć negatywny wpływ na przetwarzanie potokowe i zmienił kierunek wykonywania pętli, aby przyspieszyć operacje porównania stanu jej licznika.

for (int III = FilterSize - 1; III > 0; III--)
{
    XXX += FilterWindowWork[WindowPointer1] * FilterWindow[III];
    XXX += FilterWindowWork[WindowPointer2] * FilterWindow[III];
    WindowPointer1 = (WindowPointer1 + 1) % FilterWindowWorkSize;
    WindowPointer2 = (WindowPointer2 + FilterWindowWorkSize - 1) % FilterWindowWorkSize;
}            

Gdyby jednak zaimplementować ten algorytm tak, aby wykonywał się na wszystkich rdzeniach procesora i z użyciem instrukcji SSE, to na procesorze i5 czas jego wykonania skróciłby się ponad dziesięciokrotnie.

0

@andrzejlisek czy robiłeś jakiekolwiek profilowanie kodu?
Z moich obserwacji wynika, że ludzie często jak natrafiają problemy z performance, to skupiają się na optymalizowaniu czegoś z przekonania, a nie na podstawie pomiarów i w efekcie często marnują czas.
Dlatego radzę najpierw użyć profiler-a, by upewnić się, że faktycznie źródłem problemu jest filtr FIR.

0
MarekR22 napisał(a):

@andrzejlisek czy robiłeś jakiekolwiek profilowanie kodu?
Z moich obserwacji wynika, że ludzie często jak natrafiają problemy z performance, to skupiają się na optymalizowaniu czegoś z przekonania, a nie na podstawie pomiarów i w efekcie często marnują czas.
Dlatego radzę najpierw użyć profiler-a, by upewnić się, że faktycznie źródłem problemu jest filtr FIR.

Nie robiłem profilowania. Jak widać, odnalazłem "najcięższy" fragment drogą eliminacji.

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