14.12.2016

foto Petr Bravenec

Petr Bravenec
Twitter: @BravenecPetr
+420 777 566 384
petr.bravenec@hobrasoft.cz

V minulém článku o paralelním programování jsem nastínil, jak paralelní výpočty v Qt fungují a co je k tomu potřeba. Pro připomenutí: abychom mohli pracovat v Qt paralelně, potřebujeme nejméně dvě komponenty: vstupní data v některém z kontejnerů Qt (typicky QList) a funkci, která s jednou položkou vstupních dat dokáže provést požadovanou akci.

Odkazy

Příklad první: zmenšení obrázků

Občas mám v adresáři větší množství různých obrázků a potřebuju je zmenšit na nějakou maximální velikost. Tento problém občas skutečně řešívám a používám na to jednoduchý skript používající netpbm napsaný přímo na povelové řádce. Nutno přiznat, že skriptem na povelové řádce jsem s obrázky nikdy nemanipuloval paralelně - vždy hezky jeden po druhém. Je to nejjednodušší způsob, jak s obrázky manipulovat - psát něco podobného v C++ by mě jakživ nenapadlo. Jako příklad paralelního programování se ale taková úloha hodí velice dobře.

Vstupní data

Roli vstupních dat v příkladu zde hraje QStringList s názvy jednotlivých souborů. Názvy souborů se předávají programu jako argumenty na povelové řádce. V Qt získáme seznam argumentů velice snadno voláním QCoreApplication::arguments(). Je třeba pamatovat na to, že v první položce v kontejneru není první soubor, ale název programu - to je vlastnost operačního systému Unix. Před paralelním zmenšením obrázků je třeba první položku ze seznamu vyhodit.

QStringList files = QCoreApplication::arguments();
files.takeFirst();

Výpočetní funkce

Výpočetní funkce dostane jako parametr jednu položku ze vstupních dat a provede s touto položkou požadovanou operaci. V našem případě dostane funkce v parametru jméno některého vstupního souboru s obrázkem a jejím úkolem je tento soubor načíst, zmenšit a uložit do nového souboru s modifikovaným jménem. V Qt může být taková funkce velmi jednoduchá:

void resize(const QString& filename) {
    QImage image(filename);
    QImage scaled = (image.width() > image.height())
              ? image.scaledToWidth(196)
              : image.scaledToHeight(196);
    scaled.save("resized-"+filename);
}

Spuštění paralelního výpočtu

Pro paralelní výpočty nabízí knihovna Qt několik různých variant (map, filter, reduce, blocking a jejich kombinace). Pro naše účely je nejvhodnější blokující funkce map:

  • map vezme všechny vstupní položky a provede s nimi požadovanou operaci, tj. převede je do jiného formátu
  • blokující operace počká na kompletní provedení výpočtu
QtConcurrent::blockingMap (
    files,      // Vstupní data
    resize      // Map funkce
    );

Kompletní zdrojový tvar

#include <QCoreApplication>
#include <QtConcurrent>
#include <QImage>
#include <QDebug>

void resize(const QString& filename) {
    QImage image(filename);
    QImage scaled = (image.width() > image.height())
              ? image.scaledToWidth(196)
              : image.scaledToHeight(196);
    scaled.save("resized-"+filename);
}

int main(int argc, char **argv) {
    QCoreApplication app(argc, argv);
    QStringList files = QCoreApplication::arguments();
    files.takeFirst();
    if (files.isEmpty()) {
        qDebug() << "Usage: resize filename1.jpg filename2.jpg ...";
        return 1;
        }

    QtConcurrent::blockingMap (files, resize);
    return 0;
}

Jednodušší už to být snad ani nemůže.

Výsledky

Program jsem zkoušel zhruba na tisícovce jpg obrázků. Pro testování jsem použil osmijádrový procesor AMD FX-8350.

Výsledky jednoho vlákna

Pro porovnání jsem vytvořil jednovláknovou variantu, kde místo volání QtConcurrent::blockingMap() byl jednoduchý cyklus:

for (int i=0; i<files.size(); i++) {
    resize(files[i]);
    }

A tady je výsledek:

time ../resize *.jpg
real    0m3.507s
user    0m3.344s
sys     0m0.155s
Výsledky paralelního výpočtu

Při prvním pohledu na výsledek jsem chvíli koukal, že celkově spotřeboval program více strojového času, než trval běh aplikace. Ale potom mi docvaklo - vždyť to přece běželo paralelně na více jádrech! Zajímavé je, že paralelní varianta výpočtu spotřebovala strojového času více, než jednovláknová varianta. Takto veliký rozdíl se objevoval i při opakovaném výpočtu. I přes více spotřebovaného strojového času dokončil program svou práci několikanásobně rychleji.

time ../resize *.jpg
real    0m0.633s
user    0m4.302s
sys     0m0.171s

I přes použití osmijádrového procesoru je zrychlení jen zhruba pětinásobné. Urychlit výpočet ještě více bude pravděpodobně nemožné - velké množství času zde nejspíš padne na úkor IO operací (čtení a zápis na disk). V dalším dílu tohoto seriálu si ukážeme, že při čistě výpočetních operacích může zrychlení výpočtu mnohem lépe korespondovat s počtem jader procesoru.

Závěr

Tento článek je jedním z několika článků o paralelním programování v Qt. Sledujte tyto stránky, sledujte náš Twitter. Další díly budou následovat.

Hobrasoft s.r.o. | Kontakt