Sökresultat för

valarray vs. vector – två olika vägar för numeriska beräkningar

15 minuter i lästid
Jens Riboe
Jens Riboe
Senior/Expert Software Developer
valarray vs. vector – två olika vägar för numeriska beräkningar

När den första C++-standarden (C++98) formades, var en av ambitionerna att språket skulle innehålla byggblock för numerisk programmering. Vid den tiden var kompilatorernas autovektorisering betydligt svagare än idag, och man hoppades att ett specialiserat API skulle göra det enklare för både programmerare och kompilatorer att uttrycka och optimera matematiska operationer på hela arrayer.

Resultatet blev std::valarray: en container med fokus på elementvisa operationer, matematiska funktioner och möjligheten att arbeta på delmängder via slicing och maskning. Tanken var att man skulle kunna skriva C++-kod som såg ut som algebra – utan loopar och med potential för högeffektiv kodgenerering.

I praktiken blev valarray dock aldrig riktigt populär. Dels för att den inte fungerar som en ”riktig” STL-container (inga iteratorer, begränsad interoperabilitet), dels för att kompilatorerna snabbt blev mycket bättre på att optimera vanliga loopar över std::vector. Sedan dess har valarray förblivit något av en doldis – men det betyder inte att den är värdelös.

I den här artikeln tittar vi närmare på likheter, skillnader och när valarray faktiskt kan vara ett vettigt val.

Likheter

  • Båda lagrar homogena element (T) i en sekvens.
  • Båda stöder slumpmässig indexering med operator[].
  • Båda kan ändra storlek dynamiskt.
  • Båda kan användas för numeriska beräkningar.

Skillnader

Designmål

  • std::vector är en allmän sekvenscontainer med fullt STL-stöd: iteratorer, algoritmer, allocatorer och garanterat sammanhängande minne.
  • std::valarray är specialiserad för numerik och elementvisa operationer, med operatorer som fungerar på hela arrayer samtidigt.

Interoperabilitet

  • vector är fullt integrerad i STL och fungerar tillsammans med alla STL algoritmer.
  • valarray saknar iteratorer och fungerar inte sömlöst med <algorithm>.

Funktionalitet

  • vector har ett rikt API för insättning, radering, kapacitetshantering osv.
  • valarray erbjuder istället numeriska operationer: elementvis aritmetik, reduktionsfunktioner (sum, min, max), funktionsapplicering (apply), samt slicing och maskning.

Minnesmodell

  • vector garanterar sammanhängande minne (bra för interoperabilitet med C-APIs och SIMD).
  • valarray har historiskt haft frihet att använda andra representationer för att underlätta optimering (i praktiken ligger de flesta ändå i ett minnesblock).

När använda std::valarray?

  • När du vill skriva kort och tydlig matematik i koden.
  • När slicing eller maskning förenklar hanteringen av delmängder.
  • När du snabbt vill utföra elementvisa operationer utan att bygga loopar eller anropa STL-algoritmer.

I alla andra fall – särskilt om du behöver interoperabilitet, iteratorer, eller integration med algoritmer – är std::vector det naturliga valet.


Kodexempel

1. Normalisering

Med valarray:

#include <valarray>
#include <cmath>

std::valarray<double> x = {1, 2, 3, 4};

double mean = x.sum() / x.size();
double var  = ((x - mean) * (x - mean)).sum() / x.size();
double sd   = std::sqrt(var);

x = (x - mean) / sd; // z-normalisering

Med vector:

#include <vector>
#include <numeric>
#include <algorithm>
#include <cmath>

std::vector<double> x {1,2,3,4};

double mean = std::accumulate(x.begin(), x.end(), 0.0) / x.size();

double sqsum = 0.0;
std::for_each(x.begin(), x.end(), [&](double v){ sqsum += (v-mean)*(v-mean); });
double sd = std::sqrt(sqsum / x.size());

std::transform(x.begin(), x.end(), x.begin(), [&](double v){ return (v - mean) / sd; });

2. Selektiv tilldelning

Med valarray:

std::valarray<double> v = {-2.0, 0.5, 3.0, -1.0, 2.2};
std::valarray<bool> mask = v > 0.0;
v[mask] = 1.0; // endast positiva ersätts

Med vector:

std::vector<double> v {-2.0, 0.5, 3.0, -1.0, 2.2};
for (auto& x : v)
if (x > 0.0) x = 1.0;

3. Kolumnoperation med slice

Med valarray:

#include <valarray>
#include <cstddef>

std::size_t rows = 3, cols = 4;
std::valarray<double> M(rows * cols);
// ... fyll M ...

std::size_t j = 2;
std::slice col_j(j, rows, cols);

double mean = M[col_j].sum() / rows;
M[col_j] -= mean; // normalisera kolumnen

Med vector + manuell indexering:

std::vector<double> M(rows * cols);
// ... fyll M ...

std::size_t j = 2;
double sum = 0.0;
for (std::size_t r = 0; r < rows; ++r)
sum += M[r*cols + j];
double mean = sum / rows;

for (std::size_t r = 0; r < rows; ++r)
M[r*cols + j] -= mean;

Benchmark: vector vs valarray

Ett vanligt påstående är att valarray ska vara snabbare för numeriska operationer. Men stämmer det idag? Låt oss testa med Google Benchmark. Vi beräknar helt enkelt summan av kvadraterna för en vektor på 1 miljon element.

#include <benchmark/benchmark.h>
#include <vector>
#include <valarray>
#include <numeric>
#include <random>

static void BM_Vector_SumSquares(benchmark::State& state) {
    std::vector<double> v(state.range(0));
    std::iota(v.begin(), v.end(), 1.0);
    for (auto _ : state) {
        double sum = 0.0;
        for (double x : v) sum += x*x;
        benchmark::DoNotOptimize(sum);
    }
}
BENCHMARK(BM_Vector_SumSquares)->Arg(1'000'000);

static void BM_Valarray_SumSquares(benchmark::State& state) {
    std::valarray<double> v(state.range(0));
    for (std::size_t i=0; i<v.size(); ++i) v[i] = i+1.0;
    for (auto _ : state) {
        double sum = (v * v).sum();
        benchmark::DoNotOptimize(sum);
    }
}
BENCHMARK(BM_Valarray_SumSquares)->Arg(1'000'000);

BENCHMARK_MAIN();

Typiska resultat (Clang/GCC med -O3, Linux, x86-64)

ContainerTid per iteration
vectorca 2.8 ms
valarrayca 2.9 ms

Aktuella resultat, i Compiler Explorer

Run on (2 X 3190.45 MHz CPU s)
Load Average: 0.30, 0.29, 0.30
-------------------------------------------------------------------------
Benchmark                               Time             CPU   Iterations
-------------------------------------------------------------------------
BM_Vector_SumSquares/1000000         1.892 ms      1.286 ms          543
BM_Valarray_SumSquares/1000000       2.212 ms      1.291 ms          543

Compiler Explorer Try it yourself on Compiler Explorer

Vad betyder kolumnresultaten?

  • Time: Den faktiska klocktid som gick under testet.
  • CPU: Den beräknade CPU-tiden per iteration. Denna är ofta den mest intressanta för jämförelser eftersom den är mindre känslig för brus (OS-schemaläggning, bakgrundslaster).
  • Iterations: Antalet gånger benchmarket kördes för att få stabila siffror.

Analys

1. Vector vs Valarray:

  • vector: 1.892 ms
  • valarray: 2.212 ms → Här ser vi att valarray tar ungefär 17 % längre tid i väggtid (wall-clock time).

2. Vector vs Valarray (CPU-tid):

  • vector: 1.286 ms
  • valarray: 1.291 ms → Här är skillnaden praktiskt taget obefintlig. Vi pratar om 0.4 % skillnad, vilket ligger helt inom mätbruset.

3. Tolkning:

  • Både vector och valarray kompileras till i princip lika effektiva loopar.
  • Att väggtiden är lite högre för valarray kan bero på små skillnader i hur uttrycket (v * v).sum() översätts (tillfälliga objekt, cache-effekter etc.), men det är inte konsekvent.
  • CPU-tiden visar att i praktiken presterar de två lösningarna lika.

Benchmarket visar att det inte finns någon praktisk prestandafördel med std::valarray jämfört med std::vector i moderna kompilatorer. Eventuella små skillnader beror på implementation och är försumbara i verkliga program.

Det innebär att valet mellan vector och valarray bör göras utifrån kodens tydlighet och behov snarare än rå prestanda.

Praktiska riktlinjer

  • ✅ Använd std::vectorsom standardval: den är den mest mångsidiga och bäst integrerade containern i STL.

  • ✅ Välj std::valarray när:

    • du vill skriva kod som ser ut som matematik,
    • du behöver slicing eller maskning på arrayer,
    • du prioriterar kompakt och deklarativ kod framför maximal interoperabilitet.
  • ❌ Använd inte valarray om du behöver iteratorer, STL-algoritmer, eller interoperabilitet med externa bibliotek/C-API.

  • 💡 För moderna flerdimensionella arrayer kan std::mdspan (C++23) vara ett ännu bättre alternativ. PS, mdspan finns ännu inte implementerad.

Slutsats

std::vector är alltid förstahandsvalet för generell programmering, integration med STL och arbete nära hårdvaran.

std::valarray är en doldis som fortfarande kan skina i situationer där matematiska uttryck, slicing och maskning gör koden mer deklarativ och lättläst.

Även om prestandafördelen i praktiken ofta är liten jämfört med moderna loopar över vector, så kan valarray ibland ge en mer elegant lösning på numeriska problem. Kanske dags att damma av denna bortglömda del av STL?