C++ Wie kann man effizient 10 % aus einer List mit vielen Elementen ziehen?

foofoobar schrieb:
Ich vermute das mein Code fast komplett an der RAM-Bandbreite hängt, und da bin ich voll auf JEDEC, innerhalb der offiziellen AMD-Specs und mit ECC unterwegs. Also am alleruntersten Ende unterwegs.
Ich habe auch nochmal mit -O3 kompiliert. Siehe da, halbe Laufzeit. Also nochmehr zum Thema Performance.
Für die RAM-Bandbreite: End Creator, Time required = 7.03584 seconds Dann noch die Info dazu, dass ich alloca durch malloc ersetzt habe.
kali-hi schrieb:
Meine Variante ist langsamer, weil sich die Herangehensweisen bzw. Algorithmen unterscheiden - jedoch nicht die Datenstrukturen.
Der Vergleich von mir aus #93 zeigt für C++ gerade die beiden select_elements_a/b von dir (nur einmal noch ohne das Sortieren.) Gleichzeitig zeigt er aber die gleichen Algorithmen von foofoobar in C implementiert. Und die sind halt nochmal einiges flotter.
 
  • Gefällt mir
Reaktionen: kali-hi und Piktogramm
foofoobar schrieb:
Willst du die Weltbevölkerung dezimieren?
Es ist lediglich ein Gedankenexperiment, technische Machbarkeit und so. Was würde sich da besser zur Anschauung eignen, als die gesamte Weltbevölkerung? ;)

foofoobar schrieb:
Schau dir die Implementierung von std::vector an, der Link aus #98 liefert ja schon die entsprechenden Hinweise. Die Bequemlichkeit davon ist eben nicht for free.
Kann man solche Begriffe wie "bequem" nicht einfach weglassen (du weißt, dass diese negativ-belegt sind)... du meinst den Overhead. Ja, den gibt es - aber es wäre die Frage, ob eine deque o. Ä. für diesen Anwendungsfall schneller wäre... (oder?)
 
simpsonsfan schrieb:
zeigt für C++ gerade die beiden select_elements_a/b von dir (nur einmal noch ohne das Sortieren.) Gleichzeitig zeigt er aber die gleichen Algorithmen von foofoobar in C implementiert. Und die sind halt nochmal einiges flotter.
Mit der Programmiersprache hat das nichts zu tun. Habe den Thread nicht mehr verfolgt, aber in C++ kann man selbstredend überflüssiges herumkopieren ebenso vermeiden und üblicherweise auch während man die STL verwendet.
Zumal wir hier von pointern (= 64bit) vs. int32/int64 sprechen.
 
Zuletzt bearbeitet:
  • Gefällt mir
Reaktionen: kali-hi
kali-hi schrieb:
Es ist lediglich ein Gedankenexperiment, technische Machbarkeit und so. Was würde sich da besser zur Anschauung eignen, als die gesamte Weltbevölkerung? ;)
Ich nehme für solche Betrachtungen immer gerne die Anzahl Elektronen im Universum oder auch die Gesamtenergie des Universum nach E=mc² her.
kali-hi schrieb:
Kann man solche Begriffe wie "bequem" nicht einfach weglassen (du weißt, dass diese negativ-belegt sind)... du meinst den Overhead. Ja, den gibt es - aber es wäre die Frage, ob eine deque o. Ä. für diesen Anwendungsfall schneller wäre... (oder?)
Was du als negativ konnotiert betrachtest müssen nicht andere auch als negativ konnotiert betrachten.
Und ohne den Wunsch nach Bequemlichkeit gäbe es keine Computer, zum Glück gibt es nicht nur Calvinisten auf dieser Welt.
simpsonsfan schrieb:
Ich habe auch nochmal mit -O3 kompiliert. Siehe da, halbe Laufzeit. Also nochmehr zum Thema Performance.
Für die RAM-Bandbreite: End Creator, Time required = 7.03584 seconds Dann noch die Info dazu, dass ich alloca durch malloc ersetzt habe.
Beim Creator habe ich nicht auf Geschwindigkeit geachtet, sonst hätte ich kein sprintf() genutzt.
BeBur schrieb:
Mit der Programmiersprache hat das nichts zu tun. Habe den Thread nicht mehr verfolgt, aber in C++ kann man selbstredend überflüssiges herumkopieren ebenso vermeiden und üblicherweise auch während man die STL verwendet.
Zumal wir hier von pointern (= 64bit) vs. int32/int64 sprechen.
Es geht auch nicht um den Content sondern um die Verwaltungsstrukturen, beachte die Angaben zur Komplexität hier: https://en.cppreference.com/w/cpp/container/vector.html
 
Zuletzt bearbeitet:
  • Gefällt mir
Reaktionen: kali-hi
@BeBur Der Code von foofoobar ist flotter als der Code von kali-hi. Beide sorgen dafür, dass man eine zufällige Stichprobe der Ausgangs-Elemente kriegt. Jetzt hab ich immer noch keinen C++ Profiler da, aber die Vermutung liegt nahe, dass das Kopieren in einen neuen Vektor back_inserter(out) im C++ Code hier mit reinspielt und das bspw. eine view auf den vector schneller sein könnte. Für einen fairen Vergleich müsste man ganz konkret spezifizieren, was die Anforderungen sind und dann auch die gleichen Rohdaten hernehmen.

Aber Plain Old Data kann halt manchmal dennoch schneller sein als Container aus der STL.
Dennoch würde ich in den meisten Fällen sicher dazu raten, bestehende Standardfunktionalitäten und -container zu nutzen. Das ist bequemer (was positiv ist), weniger Fehleranfällig und in den meisten Fällen performanter als was man selbst so stricken würde (zumindest als was ich erzeugen würde.)

kali-hi schrieb:
aber es wäre die Frage, ob eine deque o. Ä. für diesen Anwendungsfall schneller wäre... (oder?)
Nein, std::vector ist für den konkreten Anwendungsfall bestimmt der beste STL-Container (wurde hier im Thread von anderen auch schon gesagt.) Aber ein einaches Array kann halt in manchen Fällen nochmal flotter sein.
Aber letzlich sind diese Dinge von der Prämisse "Gedankenexperiment: Wie kann man aus einer Liste (was immer das ist) möglichst effizient 10% der Elemente zufällig ziehen?" auch einigermaßen weit entfernt.

Denn dazu muss man mindestens mal definieren, was Liste und Ziehen bedeuten sollen. (Wohingegen zufällig in diesem Kontext vermutlich als "in einer Art und Weise, sodass jedes Element der ursprünglichen Liste nahezu die gleiche Wahrscheinlchkeit hat, gezogen zu werden", per Konvention angenommen werden kann.
 
  • Gefällt mir
Reaktionen: kali-hi
foofoobar schrieb:
Es geht auch nicht um den Content sondern um die Verwaltungsstrukturen,
( ) Du weißt, dass sich eine verlinkte (bzw. verkettete) Datenstruktur schlecht mischen lässt?
 
simpsonsfan schrieb:
Der Code von foofoobar ist flotter als der Code von kali-hi. Beide sorgen dafür, dass man eine zufällige Stichprobe der Ausgangs-Elemente kriegt. Jetzt hab ich immer noch keinen C++ Profiler da, aber die Vermutung liegt nahe, dass das Kopieren in einen neuen Vektor back_inserter(out) im C++ Code hier mit reinspielt und das bspw. eine view auf den vector schneller sein könnte.
Achso, ich dachte ihr habt meinen Ansatz verglichen mit dem von @foofoobar. Der Unterschied (Faktor 2-3 war das oder?) ist mMn zu groß um durch std::vector erklärt zu werden. So groß ist der Overhead eigentlich nicht.
Geht es um diesen Code hier? Link Da fehlt die Speicherreservierung mittels reserve. Das würde den overhead erklären, der ist aber natürlich unnötig und leicht vermeidbar.
 
@simpsonsfan Es ist verblüffend, mit einem Array (select_n_elements_a_2) (gegenüber std::vector) wird anstatt 90 % nur 70 % der Zeit benötigt...

C++:
#include <cstdint>
#include <cmath>
#include <chrono>
#include <iostream>
#include <iomanip>
#include <vector>
#include <algorithm>
#include <numeric>
#include <random>

using namespace std;

inline constexpr auto select_n_elements_a = []<typename E>(const vector<E> &in, vector<E> &out, const uint32_t n)
{
    static auto gen = default_random_engine{random_device{}()};
    vector<uint32_t> indexes(in.size());
    iota(begin(indexes), end(indexes), 0);
    shuffle(indexes.begin(), indexes.end(), gen);
    sort(indexes.begin(), indexes.begin() + n);
    for (auto it = indexes.begin(); it != indexes.begin() + n; it++)
    {
        out.push_back(in[*it]);
    }
};

inline constexpr auto select_n_elements_a_2 = []<typename E>(const vector<E> &in, vector<E> &out, const uint32_t n)
{
    if (in.size() == 0 || n == 0 || n > in.size())
    {
        return;
    }
    static auto gen = default_random_engine{random_device{}()};
    uint32_t indexes[in.size()];
    for (uint32_t i = 0; i < in.size(); i++)
    {
        indexes[i] = i;
    }
    const uint32_t limit = in.size() - 1;
    for (uint32_t i = 0; i < in.size(); i++)
    {
        uint32_t j = uniform_int_distribution<uint32_t>{0, limit}(gen);
        swap(indexes[i], indexes[j]);
    }
    sort(indexes, indexes + n);
    for (uint32_t i = 0; i < n; i++)
    {
        out.push_back(in[indexes[i]]);
    }
};

inline constexpr auto select_n_elements_b = []<typename E>(const vector<E> &in, vector<E> &out, const uint32_t n)
{
    static auto gen = default_random_engine{random_device{}()};
    sample(in.begin(), in.end(), back_inserter(out), n, gen);
};

// test until the first match, unused
template <typename T>
void test1(T select_n_elements, const uint32_t n, const uint32_t type)
{
    vector<uint32_t> in(n);
    iota(begin(in), end(in), 0);
    for (size_t i = 0; i < in.size();)
    {
        vector<uint32_t> out = {};
        select_n_elements(in, out, type == 0 ? 1 : n - 1);

        if (type == 0 && out[0] == i || type == 1 && find(out.begin(), out.end(), i) == out.end())
        {
            i++;
        }
    }
}

template <typename T>
chrono::duration<double> time1(T select_n_elements, const uint32_t n1, const uint32_t n2)
{
    vector<int> in(n1);
    iota(begin(in), end(in), 0);
    vector<int> out = {};
    auto start = chrono::system_clock::now();
    select_n_elements(in, out, n2);
    auto end = chrono::system_clock::now();
    return end - start;
}

int main()
{
    const uint32_t n1 = 1000000; // 1 million
    uint32_t n2 = 0;
    uint32_t delta = n1 / 2;
    double dist = 100;
    cout << setprecision(4);
    for (size_t i = 1; i <= 55; i++)
    {
        auto t1 = time1(select_n_elements_a_2, n1, n2);
        auto t2 = time1(select_n_elements_b, n1, n2);
        auto d = (t1 - t2) / t1 * 100.0;
        cout << "i=" << i << "\tn2=" << n2 << "\told_dist=" << dist << "\tnew_dist=" << d << "\tdelta=" << delta << endl;

        if (d < dist)
        {
            n2 += delta;
        }
        else if (d > dist)
        {
            n2 -= delta;
        }
        delta -= delta / 5;
        dist = d;
    }
    cout << "Final n2: " << n2 << endl;

    const uint32_t n11 = n1; // 1 million
    const uint32_t n21 = n2;
    auto t1 = time1(select_n_elements_a_2, n11, n21);
    auto t2 = time1(select_n_elements_b, n11, n21);
    auto diff_percent = (t1 - t2) / t1 * 100.0;
    cout << "Final test with n1=" << n11 << " n2=" << n21 << " t1=" << t1.count() << " t2=" << t2.count() << " diff_percent=" << diff_percent << "%" << endl;
    return 0;
}

Nachteil ist aber, dass Arrays nach oben beschränkt sind, d. h., mehr als ~1 Mio. Elemente passen nicht hinein.
 
kali-hi schrieb:
Nachteil ist aber, dass Arrays nach oben beschränkt sind, d. h., mehr als ~1 Mio. Elemente passen nicht hinein.
Probier mal vorm laufen lassen ein "ulimit -s unlimited" oder das entsprechende Windows Äquivalent.
kali-hi schrieb:
( ) Du weißt, dass sich eine verlinkte (bzw. verkettete) Datenstruktur schlecht mischen lässt?
Für linked Lists hatte ich bereits Vorschläge gemacht.
 
  • Gefällt mir
Reaktionen: kali-hi
foofoobar schrieb:
Probier mal vorm laufen lassen ein "ulimit -s unlimited"
Das Ergebnis dürft aber in etwa gleich sein, 70 % vs. 90 %, durch die Einsparung des Vector-Overheads.
 
BeBur schrieb:
Achso, ich dachte ihr habt meinen Ansatz verglichen mit dem von @foofoobar. Der Unterschied (Faktor 2-3 war das oder?) ist mMn zu groß um durch std::vector erklärt zu werden. So groß ist der Overhead eigentlich nicht.
Für die Berechnung der Zieladresse eines Elements in einem Array braucht es eine Addition und (eine Multiplikation oder einen shift (für n²)), sobald man da was anderes macht ist man sehr schnell bei einem vielfachen Aufwand.

Und mit ein wenig Glück werden die Adressen auch nicht in den ALUs sondern in den AGUs berechnet.
 
  • Gefällt mir
Reaktionen: Piktogramm und kali-hi
kali-hi schrieb:
( ) Du weißt, dass sich eine verlinkte (bzw. verkettete) Datenstruktur schlecht mischen lässt?
Es kommt stark darauf an wie man es umsetzt und irgendwann schaut es dann verdächtig aus, als würde man Datenstrukturen einer Datenbank nachbauen. Man kann die originalen Daten ja so indexieren:

&next&previous&content

Für ein Shuffle ziehst du dir nur die Pointer zum Content, mischst die, wählst die oberen x% und leitest davon dann wieder den Index der gleichen Form ab. Da next bzw. previous dann immer 192bit Abstand zum aktuellem Element haben, rechnet sich das dann auch recht schnell.

Das Mischen in einer Liste wäre wirklich ungünstig. Vor allem wenn es dann sowas wie wie
struct {int next; int previous, varlength data } wäre.

Edit: Wobei die Datenstruktur bzw. Verlinkte Listen sinnlos ist. Die Pointer zum Content kann man ja auch einfach nur in ein Array werfen, bei dem was der TE als Nutzdaten und Anwendung hat, lohnt so ne Liste irgendwie nicht. :rolleyes:
 
Zuletzt bearbeitet:
@Piktogramm :
Du weißt, dass es bei verlinkten Datenstrukturen keine Zusicherung (Assertion bzw. wp-Kalkül) gibt, dass alle "Zeiger" die gleiche Größe und/oder aufeinanderfolgend sind, also keine arithmetischen Berechnungen möglich sind, wodurch sich zwei beliebige Referenzen nicht einfach vertauschen lassen - und es andernfalls keine verlinkte Datenstruktur mehr wäre? (Rhetorische Frage)

Analog gilt das auch für das Sortieren... Das geht zwar, zum Beispiel mit BubbleSort, dauert aber lang... Deshalb wird das ja auch vorher in eine Hilfsstruktur (Vektor, Array...) kopiert, und dann wieder zurück
 
Wer legt mit einer Implementierung für linked Lists hier vor die es zu schlagen gilt?
 
  • Gefällt mir
Reaktionen: kali-hi
foofoobar schrieb:
mit einer Implementierung für linked Lists
Ich nicht... so etwas müssen unsere Studis machen 🤣

Außerdem, eine LL, die sich wie ein Vector handhaben lässt, ist auch einer ;)
 
Ich bin da bei @kali-hi . Klar, ein dynamisches array, das zusätzlich und unnötigerweise Pointer auf das nächste Element enthält ist theoretisch auch eine linked list... ist aber ziemlich witzlos.
"Wer baut die für uns schnellste linked list" ist daher mMn auch keine wohldefinierte Aufgabe.
 
  • Gefällt mir
Reaktionen: kali-hi
Ich konnte das bislang noch nicht auf "Herz und Nieren" testen, aber meintest du so etwas in der Art?

C++:
#include <cstdint>
#include <cmath>
#include <chrono>
#include <iostream>
#include <iomanip>
#include <vector>
#include <algorithm>
#include <numeric>
#include <random>
#include <deque>

using namespace std;

template <typename T>
class RandomDeque : public deque<T>
{
public:
    RandomDeque() : deque<T>() {}
    RandomDeque(const vector<T> &list) : deque<T>()
    {
        for (const auto &item : list)
        {
            this->push_random(item);
        }
    }
    void push_random(const T &value)
    {
        static auto gen = default_random_engine{random_device{}()};
        uniform_int_distribution<size_t> dist(0, 1);
        size_t index = dist(gen);
        if (index == 0)
        {
            this->push_front(value);
        }
        else
        {
            this->push_back(value);
        }
    }
    void pop_n_elements(vector<T> &out, const uint32_t n)
    {
        if (n > this->size())
        {
            throw runtime_error("Not enough elements to pop");
        }
        for (uint32_t i = 0; i < n; i++)
        {
            out.push_back(this->front());
            this->pop_front();
        }
    }
};

inline constexpr auto select_n_elements_a_1 = []<typename E>(const vector<E> &in, vector<E> &out, const uint32_t n)
{
    static auto gen = default_random_engine{random_device{}()};
    vector<uint32_t> indexes(in.size());
    iota(begin(indexes), end(indexes), 0);
    shuffle(indexes.begin(), indexes.end(), gen);
    sort(indexes.begin(), indexes.begin() + n);
    for (auto it = indexes.begin(); it != indexes.begin() + n; it++)
    {
        out.push_back(in[*it]);
    }
};

inline constexpr auto select_n_elements_a_2 = []<typename E>(const vector<E> &in, vector<E> &out, const uint32_t n)
{
    if (in.size() == 0 || n == 0 || n > in.size())
    {
        return;
    }
    static auto gen = default_random_engine{random_device{}()};
    uint32_t indexes[in.size()];
    for (uint32_t i = 0; i < in.size(); i++)
    {
        indexes[i] = i;
    }
    const uint32_t limit = in.size() - 1;
    for (uint32_t i = 0; i < in.size(); i++)
    {
        uint32_t j = uniform_int_distribution<uint32_t>{0, limit}(gen);
        swap(indexes[i], indexes[j]);
    }
    sort(indexes, indexes + n);
    for (uint32_t i = 0; i < n; i++)
    {
        out.push_back(in[indexes[i]]);
    }
};

inline constexpr auto select_n_elements_a_3 = []<typename E>(const vector<E> &in, vector<E> &out, const uint32_t n)
{
    if (in.size() == 0 || n == 0 || n > in.size())
    {
        return;
    }
    RandomDeque<E> rdeque(in);
    rdeque.pop_n_elements(out, n);
};

inline constexpr auto select_n_elements_b = []<typename E>(const vector<E> &in, vector<E> &out, const uint32_t n)
{
    static auto gen = default_random_engine{random_device{}()};
    sample(in.begin(), in.end(), back_inserter(out), n, gen);
};

// test until the first match, unused
template <typename T>
void test1(T select_n_elements, const uint32_t n, const uint32_t type)
{
    vector<uint32_t> in(n);
    iota(begin(in), end(in), 0);
    for (size_t i = 0; i < in.size();)
    {
        vector<uint32_t> out = {};
        select_n_elements(in, out, type == 0 ? 1 : n - 1);

        if (type == 0 && out[0] == i || type == 1 && find(out.begin(), out.end(), i) == out.end())
        {
            i++;
        }
    }
}

template <typename T>
chrono::duration<double> time1(T select_n_elements, const uint32_t n1, const uint32_t n2)
{
    vector<int> in(n1);
    iota(begin(in), end(in), 0);
    vector<int> out = {};
    auto start = chrono::system_clock::now();
    select_n_elements(in, out, n2);
    auto end = chrono::system_clock::now();
    return end - start;
}

int main()
{
    {
        // Example usage
        vector<string> in = {"a", "b", "c", "d", "e", "f", "g", "h", "i", "j"};
        vector<string> out = {};
        select_n_elements_a_3(in, out, 4);
        for (const auto &item : out)
        {
            cout << item << " ";
        }
        cout << endl;
    }

    const uint32_t n1 = 100000; // 100 thousand
    uint32_t n2 = 0;
    uint32_t delta = n1 / 2;
    double dist = 100;
    cout << setprecision(4);
    for (size_t i = 1; i <= 55; i++)
    {
        auto t1 = time1(select_n_elements_a_3, n1, n2);
        auto t2 = time1(select_n_elements_b, n1, n2);
        auto d = (t1 - t2) / t1 * 100.0;
        cout << "i=" << i << "\tn2=" << n2 << "\told_dist=" << dist << "\tnew_dist=" << d << "\tdelta=" << delta << endl;

        if (d < dist)
        {
            n2 += delta;
        }
        else if (d > dist)
        {
            n2 -= delta;
        }
        delta -= delta / 5;
        dist = d;
    }
    cout << "Final n2: " << n2 << endl;

    const uint32_t n11 = n1 * 10; // 1 million
    const uint32_t n21 = n2 * 10;
    auto t1 = time1(select_n_elements_a_3, n11, n21);
    auto t2 = time1(select_n_elements_b, n11, n21);
    auto diff_percent = (t1 - t2) / t1 * 100.0;
    cout << "Final test with n1=" << n11 << " n2=" << n21 << " t1=" << t1.count() << " t2=" << t2.count() << " diff_percent=" << diff_percent << "%" << endl;
    return 0;
}

RandomDeque scheint schneller zu sein als alles bisherige. Ich weiß aber nicht, ob es auch dem Zufall genügt?
 
kali-hi schrieb:
Ich konnte das bislang noch nicht auf "Herz und Nieren" testen, aber meintest du so etwas in der Art?
RandomDeque scheint schneller zu sein als alles bisherige. Ich weiß aber nicht, ob es auch dem Zufall genügt?
Abgesehen davon das ein Vector keine linked List ist, sieht das so aus als würdest die Eingangsdaten zerstören.
 
Zurück
Oben