it-swarm.com.de

1D- oder 2D-Array, was ist schneller?

Ich muss ein 2D-Feld (Achsen x, y) darstellen und habe ein Problem: Soll ich ein 1D-Array oder ein 2D-Array verwenden?

Ich kann mir vorstellen, dass die Neuberechnung von Indizes für 1D-Arrays (y + x * n) langsamer sein könnte als die Verwendung von 2D-Arrays (x, y), aber ich könnte mir vorstellen, dass 1D sich im CPU-Cache befinden könnte.

Ich habe etwas gegoogelt, aber nur Seiten gefunden, die sich auf statische Felder beziehen (und dass 1D und 2D grundsätzlich gleich sind). Aber meine Arrays müssen mich dynamisch machen.

Was ist also? 

  1. schneller, 
  2. kleiner (RAM) 

dynamische 1D-Arrays oder dynamische 2D-Arrays?

Vielen Dank :)

53
graywolf

tl; dr: Sie sollten wahrscheinlich einen eindimensionalen Ansatz verwenden.

Hinweis: Man kann die Leistung nicht detailliert untersuchen, wenn man dynamische 1d- oder dynamische 2d-Speichermuster vergleicht, ohne Bücher zu füllen, da die Leistung des Codes von einer sehr großen Anzahl von Parametern abhängt. Profil wenn möglich.

1. Was ist schneller?

Für dichte Matrizen ist der 1D-Ansatz wahrscheinlich schneller, da er eine bessere Speicherlokalität und einen geringeren Overhead für Zuweisung und Freigabe bietet.

2. Was ist kleiner?

Dynamic-1D benötigt weniger Speicher als der 2D-Ansatz. Letzteres erfordert auch mehr Zuweisungen.

Bemerkungen

Ich habe eine ziemlich lange Antwort mit mehreren Gründen dargelegt, aber ich möchte zuerst einige Anmerkungen zu Ihren Annahmen machen.

Ich kann mir vorstellen, dass die Neuberechnung von Indizes für 1D-Arrays (y + x * n) langsamer sein könnte als die Verwendung von 2D-Arrays (x, y).

Vergleichen wir diese beiden Funktionen:

int get_2d (int **p, int r, int c) { return p[r][c]; }
int get_1d (int *p, int r, int c)  { return p[c + C*r]; }

Die von Visual Studio 2015 RC für diese Funktionen (mit aktivierten Optimierungen) generierte (nicht inlinierte) Assembly lautet:

[email protected]@[email protected] PROC
Push    ebp
mov ebp, esp
mov eax, DWORD PTR _c$[ebp]
lea eax, DWORD PTR [eax+edx*4]
mov eax, DWORD PTR [ecx+eax*4]
pop ebp
ret 0

[email protected]@[email protected] PROC
Push ebp
mov ebp, esp
mov ecx, DWORD PTR [ecx+edx*4]
mov eax, DWORD PTR _c$[ebp]
mov eax, DWORD PTR [ecx+eax*4]
pop ebp
ret 0

Der Unterschied ist mov (2d) gegen lea (1d). Ersteres hat eine Latenz von 3 Zyklen und einen maximalen Durchsatz von 2 pro Zyklus, während letzteres eine Latenz von 2 Zyklen und einen maximalen Durchsatz von 3 pro Zyklus hat. (Gemäß Instruction Tables - Agner Fog Da die Unterschiede geringfügig sind, sollte die Index-Neuberechnung meines Erachtens keinen großen Leistungsunterschied bewirken. Ich gehe davon aus, dass es sehr unwahrscheinlich ist, dass dieser Unterschied selbst festgestellt werden kann der Engpass in jedem Programm.

Dies bringt uns zum nächsten (und interessanteren) Punkt:

... aber ich könnte mir vorstellen, dass 1D im CPU-Cache sein könnte ...

Stimmt, aber 2d könnte sich auch im CPU-Cache befinden. Siehe Die Nachteile: Speicherlokalität für eine Erklärung, warum 1d immer noch besser ist.

Die lange Antwort, oder warum dynamische zweidimensionale Datenspeicherung (Zeiger-zu-Zeiger oder Vektor-von-Vektor) ist "schlecht" für einfach/kleine Matrizen.

Hinweis: Hier geht es um dynamische Arrays/Zuweisungsschemata [malloc/new/vector etc.]. Ein statisches 2d-Array ist ein zusammenhängender Speicherblock und unterliegt daher nicht den Nachteilen, die ich hier präsentieren werde.

Das Problem

Um zu verstehen, warum ein dynamisches Array von dynamischen Arrays oder ein Vektor von Vektoren höchstwahrscheinlich nicht das Datenspeichermuster der Wahl ist, müssen Sie das Speicherlayout solcher Strukturen verstehen.

Beispielfall unter Verwendung der Zeigersyntax

int main (void)
{
    // allocate memory for 4x4 integers; quick & dirty
    int ** p = new int*[4];
    for (size_t i=0; i<4; ++i) p[i] = new int[4]; 

    // do some stuff here, using p[x][y] 

    // deallocate memory
    for (size_t i=0; i<4; ++i) delete[] p[i];
    delete[] p;
}

Die Nachteile

Speicherort

Für diese „Matrix“ weisen Sie einen Block mit vier Zeigern und vier Blöcke mit vier ganzen Zahlen zu. Alle Zuordnungen haben nichts zu tun und können daher zu einer beliebigen Speicherposition führen.

Das folgende Bild gibt Ihnen eine Vorstellung davon, wie der Speicher aussehen könnte.

Für den echten 2d Fall:

  • Das violette Quadrat ist die Speicherposition, die p selbst einnimmt.
  • Die grünen Quadrate setzen den Speicherbereich zusammen, auf den p zeigt (4 x int*).
  • Die 4 Bereiche von 4 zusammenhängenden blauen Quadraten sind diejenigen, auf die jeder int* Des grünen Bereichs zeigt

Für den Fall 2d auf 1d abgebildet:

  • Das grüne Quadrat ist der einzige erforderliche Zeiger int *
  • Die blauen Quadrate bilden den Speicherbereich für alle Matrixelemente (16 x int).

Real 2d vs mapped 2d memory layout

Dies bedeutet, dass Sie (bei Verwendung des linken Layouts) wahrscheinlich eine schlechtere Leistung als bei einem zusammenhängenden Speichermuster (wie auf der rechten Seite zu sehen) feststellen werden, z. B. aufgrund von Caching.

Nehmen wir an, eine Cache-Zeile ist "die Datenmenge, die auf einmal in den Cache übertragen wird", und stellen wir uns ein Programm vor, das nacheinander auf die gesamte Matrix zugreift.

Wenn Sie eine richtig ausgerichtete 4-mal-4-Matrix mit 32-Bit-Werten haben, kann ein Prozessor mit einer 64-Byte-Cache-Zeile (typischer Wert) die Daten "einmalig" verarbeiten (4 * 4 * 4 = 64 Byte). Wenn Sie mit der Verarbeitung beginnen und sich die Daten noch nicht im Cache befinden, tritt ein Cache-Fehler auf und die Daten werden aus dem Hauptspeicher abgerufen. Diese Last kann die gesamte Matrix auf einmal abrufen, da sie genau dann in eine Cache-Zeile passt, wenn sie zusammenhängend gespeichert (und richtig ausgerichtet) ist. Während der Verarbeitung dieser Daten wird es wahrscheinlich keine Fehler mehr geben.

Im Falle eines dynamischen "echten zweidimensionalen" Systems mit nicht zusammenhängenden Stellen jeder Zeile/Spalte muss der Prozessor jede Speicherstelle separat laden. Obwohl nur 64 Bytes erforderlich sind, würde das Laden von 4 Cache-Zeilen für 4 unabhängige Speicherpositionen - im schlimmsten Fall - tatsächlich 256 Bytes übertragen und 75% Durchsatzbandbreite verschwenden. Wenn Sie die Daten unter Verwendung des 2d-Schemas verarbeiten, werden Sie erneut (falls nicht bereits zwischengespeichert) einen Cache-Fehler beim ersten Element feststellen. Jetzt befindet sich jedoch nur die erste Zeile/Spalte nach dem ersten Laden aus dem Hauptspeicher im Cache, da sich alle anderen Zeilen an einer anderen Stelle im Speicher befinden und nicht neben der ersten. Sobald Sie eine neue Zeile/Spalte erreichen, kommt es erneut zu einem Cache-Miss und das nächste Laden aus dem Hauptspeicher wird durchgeführt.

Kurz gesagt: Das 2d-Muster weist eine höhere Wahrscheinlichkeit für Cache-Ausfälle auf, da das 1d-Schema aufgrund der Datenlokalität ein besseres Leistungspotenzial bietet.

Häufige Zuteilung/Freigabe

  • Es sind bis zu N + 1 (4 + 1 = 5) Zuweisungen (entweder mit new, malloc, allocator :: allocate oder was auch immer) erforderlich, um die gewünschte NxM (4 × 4) -Matrix zu erstellen.
  • Die gleiche Anzahl von ordnungsgemäßen entsprechenden Freigabevorgängen muss ebenfalls angewendet werden.

Daher ist es teurer, solche Matrizen im Gegensatz zu einem einzelnen Zuordnungsschema zu erstellen/zu kopieren.

Mit zunehmender Zeilenanzahl wird dies noch schlimmer.

Overhead beim Speicherverbrauch

Ich werde eine Größe von 32 Bits für int und 32 Bits für Zeiger asumme. (Hinweis: Systemabhängigkeit.)

Erinnern wir uns: Wir wollen eine 4 × 4-Int-Matrix speichern, was 64 Bytes bedeutet.

Für eine NxM-Matrix, die mit dem dargestellten Zeiger-zu-Zeiger-Schema gespeichert wurde, verwenden wir

  • N*M*sizeof(int) [die aktuellen blauen Daten] +
  • N*sizeof(int*) [die grünen Zeiger] +
  • sizeof(int**) [die violette Variable p] Bytes.

Das macht im vorliegenden Beispiel 4*4*4 + 4*4 + 4 = 84 Bytes und es wird noch schlimmer, wenn std::vector<std::vector<int>> Verwendet wird. Es werden N * M * sizeof(int) + N * sizeof(vector<int>) + sizeof(vector<vector<int>>) Bytes benötigt, dh insgesamt 4*4*4 + 4*16 + 16 = 144 Bytes, anstelle von 64 Bytes für 4 x 4 int.

Zusätzlich kann (abhängig vom verwendeten Allokator) jede einzelne Allokation einen zusätzlichen Speicher-Overhead von 16 Bytes haben (und wird dies höchstwahrscheinlich auch tun). (Einige "Infobytes", die die Anzahl der zugewiesenen Bytes für die ordnungsgemäße Freigabe speichern.)

Das heißt, der schlimmste Fall ist:

N*(16+M*sizeof(int)) + 16+N*sizeof(int*) + sizeof(int**)
= 4*(16+4*4) + 16+4*4 + 4 = 164 bytes ! _Overhead: 156%_

Der Anteil des Overhead verringert sich mit zunehmender Größe der Matrix, bleibt aber weiterhin vorhanden.

Risiko von Speicherlecks

Die Anzahl der Zuordnungen erfordert eine angemessene Ausnahmebehandlung, um Speicherlecks zu vermeiden, wenn eine der Zuordnungen fehlschlägt! Sie müssen die zugewiesenen Speicherblöcke nachverfolgen und dürfen sie nicht vergessen, wenn Sie die Speicherzuweisung aufheben.

Wenn new nicht genügend Speicherplatz hat und die nächste Zeile nicht zugeordnet werden kann (besonders wahrscheinlich, wenn die Matrix sehr groß ist), wird von new ein std::bad_alloc Geworfen.

Beispiel:

In dem oben erwähnten Beispiel für "Neu/Löschen" sehen wir uns mit etwas mehr Code konfrontiert, wenn wir im Falle von bad_alloc - Ausnahmen Leckagen vermeiden möchten.

  // allocate memory for 4x4 integers; quick & dirty
  size_t const N = 4;
  // we don't need try for this allocation
  // if it fails there is no leak
  int ** p = new int*[N];
  size_t allocs(0U);
  try 
  { // try block doing further allocations
    for (size_t i=0; i<N; ++i) 
    {
      p[i] = new int[4]; // allocate
      ++allocs; // advance counter if no exception occured
    }
  }
  catch (std::bad_alloc & be)
  { // if an exception occurs we need to free out memory
    for (size_t i=0; i<allocs; ++i) delete[] p[i]; // free all alloced p[i]s
    delete[] p; // free p
    throw; // rethrow bad_alloc
  }
  /*
     do some stuff here, using p[x][y] 
  */
  // deallocate memory accoding to the number of allocations
  for (size_t i=0; i<allocs; ++i) delete[] p[i];
  delete[] p;

Zusammenfassung

Es gibt Fälle, in denen "echte 2D" -Speicherlayouts passen und sinnvoll sind (dh wenn die Anzahl der Spalten pro Zeile nicht konstant ist), aber in den einfachsten und gebräuchlichsten Fällen der 2D-Datenspeicherung die Komplexität Ihres Codes nur aufblähen und die Leistung reduzieren und Speichereffizienz Ihres Programms.

Alternative

Sie sollten einen zusammenhängenden Speicherblock verwenden und Ihre Zeilen diesem Block zuordnen.

Die "C++ - Methode" besteht wahrscheinlich darin, eine Klasse zu schreiben, die Ihr Gedächtnis verwaltet und dabei wichtige Dinge wie berücksichtigt

Beispiel

Um eine Vorstellung davon zu bekommen, wie eine solche Klasse aussehen könnte, folgt ein einfaches Beispiel mit einigen grundlegenden Funktionen:

  • 2d-size-constructible
  • 2d-resizable
  • operator(size_t, size_t) für den Zugriff auf 2-zeilige Hauptelemente
  • at(size_t, size_t) für aktivierten Zugriff auf 2-Zeilen-Hauptelemente
  • Erfüllt die Konzeptanforderungen für Container

Quelle:

#include <vector>
#include <algorithm>
#include <iterator>
#include <utility>

namespace matrices
{

  template<class T>
  class simple
  {
  public:
    // misc types
    using data_type  = std::vector<T>;
    using value_type = typename std::vector<T>::value_type;
    using size_type  = typename std::vector<T>::size_type;
    // ref
    using reference       = typename std::vector<T>::reference;
    using const_reference = typename std::vector<T>::const_reference;
    // iter
    using iterator       = typename std::vector<T>::iterator;
    using const_iterator = typename std::vector<T>::const_iterator;
    // reverse iter
    using reverse_iterator       = typename std::vector<T>::reverse_iterator;
    using const_reverse_iterator = typename std::vector<T>::const_reverse_iterator;

    // empty construction
    simple() = default;

    // default-insert rows*cols values
    simple(size_type rows, size_type cols)
      : m_rows(rows), m_cols(cols), m_data(rows*cols)
    {}

    // copy initialized matrix rows*cols
    simple(size_type rows, size_type cols, const_reference val)
      : m_rows(rows), m_cols(cols), m_data(rows*cols, val)
    {}

    // 1d-iterators

    iterator begin() { return m_data.begin(); }
    iterator end() { return m_data.end(); }
    const_iterator begin() const { return m_data.begin(); }
    const_iterator end() const { return m_data.end(); }
    const_iterator cbegin() const { return m_data.cbegin(); }
    const_iterator cend() const { return m_data.cend(); }
    reverse_iterator rbegin() { return m_data.rbegin(); }
    reverse_iterator rend() { return m_data.rend(); }
    const_reverse_iterator rbegin() const { return m_data.rbegin(); }
    const_reverse_iterator rend() const { return m_data.rend(); }
    const_reverse_iterator crbegin() const { return m_data.crbegin(); }
    const_reverse_iterator crend() const { return m_data.crend(); }

    // element access (row major indexation)
    reference operator() (size_type const row,
      size_type const column)
    {
      return m_data[m_cols*row + column];
    }
    const_reference operator() (size_type const row,
      size_type const column) const
    {
      return m_data[m_cols*row + column];
    }
    reference at() (size_type const row, size_type const column)
    {
      return m_data.at(m_cols*row + column);
    }
    const_reference at() (size_type const row, size_type const column) const
    {
      return m_data.at(m_cols*row + column);
    }

    // resizing
    void resize(size_type new_rows, size_type new_cols)
    {
      // new matrix new_rows times new_cols
      simple tmp(new_rows, new_cols);
      // select smaller row and col size
      auto mc = std::min(m_cols, new_cols);
      auto mr = std::min(m_rows, new_rows);
      for (size_type i(0U); i < mr; ++i)
      {
        // iterators to begin of rows
        auto row = begin() + i*m_cols;
        auto tmp_row = tmp.begin() + i*new_cols;
        // move mc elements to tmp
        std::move(row, row + mc, tmp_row);
      }
      // move assignment to this
      *this = std::move(tmp);
    }

    // size and capacity
    size_type size() const { return m_data.size(); }
    size_type max_size() const { return m_data.max_size(); }
    bool empty() const { return m_data.empty(); }
    // dimensionality
    size_type rows() const { return m_rows; }
    size_type cols() const { return m_cols; }
    // data swapping
    void swap(simple &rhs)
    {
      using std::swap;
      m_data.swap(rhs.m_data);
      swap(m_rows, rhs.m_rows);
      swap(m_cols, rhs.m_cols);
    }
  private:
    // content
    size_type m_rows{ 0u };
    size_type m_cols{ 0u };
    data_type m_data{};
  };
  template<class T>
  void swap(simple<T> & lhs, simple<T> & rhs)
  {
    lhs.swap(rhs);
  }
  template<class T>
  bool operator== (simple<T> const &a, simple<T> const &b)
  {
    if (a.rows() != b.rows() || a.cols() != b.cols())
    {
      return false;
    }
    return std::equal(a.begin(), a.end(), b.begin(), b.end());
  }
  template<class T>
  bool operator!= (simple<T> const &a, simple<T> const &b)
  {
    return !(a == b);
  }

}

Beachten Sie hier einige Dinge:

  • T muss die Anforderungen der verwendeten Memberfunktionen std::vector erfüllen
  • operator() führt keine "of of range" -Prüfungen durch
  • Sie müssen Ihre Daten nicht selbst verwalten
  • Es sind keine Destruktor-, Kopierkonstruktor- oder Zuweisungsoperatoren erforderlich

Sie müssen sich also nicht um die ordnungsgemäße Speicherverwaltung für jede Anwendung kümmern, sondern nur einmal für die Klasse, die Sie schreiben.

Beschränkungen

Es kann Fälle geben, in denen eine dynamische "echte" zweidimensionale Struktur günstig ist. Dies ist beispielsweise der Fall, wenn

  • die Matrix ist sehr groß und dünn (wenn eine der Zeilen nicht einmal zugeordnet werden muss, sondern mit einem Nullptr behandelt werden kann) oder wenn
  • die Zeilen haben nicht die gleiche Anzahl von Spalten (das heißt, wenn Sie überhaupt keine Matrix, sondern ein anderes zweidimensionales Konstrukt haben).
180
Pixelchemist

Wenn nicht Sie über statische Arrays sprechen, ist 1D schneller .

Hier ist das Speicherlayout eines 1D-Arrays (std::vector<T>):

+---+---+---+---+---+---+---+---+---+
|   |   |   |   |   |   |   |   |   |
+---+---+---+---+---+---+---+---+---+

Und das Gleiche gilt für ein dynamisches 2D-Array (std::vector<std::vector<T>>):

+---+---+---+
| * | * | * |
+-|-+-|-+-|-+
  |   |   V
  |   | +---+---+---+
  |   | |   |   |   |
  |   | +---+---+---+
  |   V
  | +---+---+---+
  | |   |   |   |
  | +---+---+---+
  V
+---+---+---+
|   |   |   |
+---+---+---+

Offensichtlich verliert der 2D-Fall die Cache-Lokalität und verbraucht mehr Speicher. Es führt auch eine zusätzliche Indirektion ein (und damit einen zusätzlichen Zeiger, der folgt), aber das erste Array hat den Aufwand, die Indizes zu berechnen, so dass sich diese mehr oder weniger ausgleichen.

16
Konrad Rudolph

Statische 1D- und 2D-Arrays

  • Größe: Beide benötigen dieselbe Speichermenge.

  • Speed: Sie können davon ausgehen, dass es keinen Geschwindigkeitsunterschied gibt, da der Speicher für diese beiden Arrays zusammenhängend sein sollte Brocken verbreiten sich im Speicher). (Dies kann jedoch abhängig vom Compiler .__ sein.)

Dynamische 1D- und 2D-Arrays

  • Größe: Das 2D-Array benötigt ein wenig mehr Speicher als das 1D-Array, da die Zeiger im 2D-Array auf die Menge der zugewiesenen 1D-Arrays verweisen. (Dieses winzige Bit ist nur winzig, wenn wir von wirklich großen Arrays sprechen. Bei kleinen Arrays könnte das winzige Bit relativ groß sein.)

  • Geschwindigkeit: Das 1D-Array ist möglicherweise schneller als das 2D-Array, da der Speicher für das 2D-Array nicht zusammenhängend ist, sodass Cache-Fehler ein Problem darstellen.


Verwenden Sie, was funktioniert und am logischsten erscheint, und wenn Sie Geschwindigkeitsprobleme haben, dann Refactor.

8

Bei den vorhandenen Antworten werden nur 1-D-Arrays mit Arrays von Zeigern verglichen.

In C (aber nicht in C++) gibt es eine dritte Option. Sie können ein zusammenhängendes 2-D-Array haben, das dynamisch zugewiesen wird und Laufzeitdimensionen hat:

int (*p)[num_columns] = malloc(num_rows * sizeof *p);

und darauf wird wie p[row_index][col_index] zugegriffen. 

Ich würde erwarten, dass dies eine sehr ähnliche Leistung wie das 1-D-Array aufweist, aber es gibt Ihnen eine schönere Syntax für den Zugriff auf die Zellen.

In C++ können Sie etwas Ähnliches erreichen, indem Sie eine Klasse definieren, die ein 1-D-Array intern verwaltet, jedoch über eine 2-D-Array-Zugriffssyntax mit überladenen Operatoren verfügbar macht. Wieder würde ich davon ausgehen, dass sie eine ähnliche oder identische Leistung zum einfachen 1-D-Array haben.

5
M.M

Ein weiterer Unterschied zwischen 1D- und 2D-Arrays tritt bei der Speicherzuordnung auf. Wir können nicht sicher sein, dass Mitglieder eines 2D-Arrays sequentiell sind.

4
Polymorphism

Es hängt wirklich davon ab, wie Ihr 2D-Array implementiert wird. 

int a[200], b[10][20], *c[10], *d[10];
for (ii = 0; ii < 10; ++ii)
{
   c[ii] = &b[ii][0];
   d[ii] = (int*) malloc(20 * sizeof(int));    // The cast for C++ only.
}

Es gibt drei Implementierungen: b, c und d Es wird keine großen Unterschiede beim Zugriff auf b [x] [y] oder a [x * 20 + y] geben, da Sie die eine Berechnung durchführen und die andere ist der Compiler, der es für Sie erledigt. c [x] [y] und d [x] [y] sind langsamer, da die Maschine die Adresse suchen muss, auf die c [x] zeigt, und dann auf das y-te Element von dort aus zugreift. Es ist keine einfache Berechnung. Auf einigen Maschinen (z. B. AS400, der 36 Byte (keine Bit) Zeiger hat) ist der Zeigerzugriff extrem langsam. Es ist alles abhängig von der verwendeten Architektur. Bei x86-Architekturen sind a und b die gleiche Geschwindigkeit, c und d sind langsamer als b.

1
cup

Ich liebe die ausführliche Antwort von Pixelchemist . Eine einfachere Version dieser Lösung kann wie folgt aussehen. Zuerst geben Sie die Maße an:

constexpr int M = 16; // rows
constexpr int N = 16; // columns
constexpr int P = 16; // planes

Als Nächstes erstellen Sie einen Alias ​​und erhalten und setzen Methoden:

template<typename T>
using Vector = std::vector<T>;

template<typename T>
inline T& set_elem(vector<T>& m_, size_t i_, size_t j_, size_t k_)
{
    // check indexes here...
    return m_[i_*N*P + j_*P + k_];
}

template<typename T>
inline const T& get_elem(const vector<T>& m_, size_t i_, size_t j_, size_t k_)
{
    // check indexes here...
    return m_[i_*N*P + j_*P + k_];
}

Schließlich kann ein Vektor wie folgt erstellt und indiziert werden:

Vector array3d(M*N*P, 0);            // create 3-d array containing M*N*P zero ints
set_elem(array3d, 0, 0, 1) = 5;      // array3d[0][0][1] = 5
auto n = get_elem(array3d, 0, 0, 1); // n = 5

Durch die Definition der Vektorgröße bei der Initialisierung wird optimale Leistung erreicht. Diese Lösung wird aus dieser Antwort geändert. Die Funktionen können überlastet sein, um unterschiedliche Dimensionen mit einem einzelnen Vektor zu unterstützen. Der Nachteil dieser Lösung besteht darin, dass die Parameter M, N, P implizit an die Funktionen get und set übergeben werden. Dies kann gelöst werden, indem die Lösung innerhalb einer Klasse implementiert wird, wie dies von Pixelchemist der Fall ist.

0
Adam Erickson