Native Array or Std::Array; That Is the Question!
Native array is more static, which std::array is dynamic. This article focuses on static performance specifically.
Join the DZone community and get the full member experience.
Join For FreeI'm a big advocate for using the C++ standard library as much as possible. Modern library implementations are fast and stable today, they are easy to use, have clear interfaces, and have unified semantics. This wasn't always the case but it has been over the past few years. here, I'm going to take a look at array-like collections in C++ — specifically the std::array and std::vector types.
Both of these types integrate with various standard library algorithms in similar ways. The first is purely static; however, while the second is dynamic. Today we're going to look at static performance specifically.
You may also like: Arrays.hashCode() Vs. Objects.hash()
First, let's take a look at overall usability. Frankly, as the STL has tons of support for legacy C types, they both work pretty well:
for(const auto& i : std_numbers)
{
std::cout << i << std::endl;
}
for(const auto& i : a_numbers)
{
std::cout << i << std::endl;
}
auto min = std::min_element(std_numbers.begin(), std_numbers.end());
std::cout << "std Min: " << *min << std::endl;
min = std::min_element(std::begin(a_numbers), std::end(a_numbers));
std::cout << "a Min: " << *min << std::endl;
There are only small differences here — specifically when invoking an algorithm (i.e. std::min_element(.)). The std::array type has integrated support for iterators; we need to use adapter functions from the STL to generate an iterator from an array. This particular difference is trivial.
There's another that's not.
You can access data in an std::array using either bracket notation or via the std::array::at(.) method:
xxxxxxxxxx
std_numbers[0] = 100;
for(const auto& i : std_numbers)
{
std::cout << i << std::endl;
}
std_numbers.at(0) = 1000;
for(const auto& i : std_numbers)
{
std::cout << i << std::endl;
}
a_numbers[0] = 100;
for(const auto& i : a_numbers)
{
std::cout << i << std::endl;
}
Std::array::at(.) accesses elements in an array with bounds checking. Bracket notation does not, and neither does native arrays. This is a big deal! Out of bounds errors in arrays is a major source of significant, exploitable security vulnerabilities in software. You can avoid those issues with std::array::at(.). That by itself is reason to use std::array.
What about performance?
Let's look at a few specific operations — creation, assignment, retrieval, and copying. Here's the code we'll use:
xxxxxxxxxx
template<typename T>
void evaluation_engine(const T& f, const std::string& label = "Container")
{
std::vector<double> samples;
for (auto i = 0; i < sample_max; ++i)
{
const auto start_time = std::chrono::high_resolution_clock::now();
for (auto j = 0; j < loop_max; ++j)
{
f();
}
const auto stop_time = std::chrono::high_resolution_clock::now();
const auto duration = stop_time — start_time;
samples.push_back(duration.count());
}
print_statistics(samples, label);
}
The sample_max is 100, and the loop_max 1000. This function takes a lambda expression or function and evaluates it, timing accesses. print_statistics() will calculate a few statistics and print them (omitted).
Our creation tests are pretty straightforward:
xxxxxxxxxx
evaluation_engine([]()
{
constexpr std::array<int, 5> numbers {0, 1, 2, 3, 4};
}, "STL Creation");
evaluation_engine([]()
{
constexpr int numbers[] {0, 1, 2, 3, 4};
}, "Array");
This gives us:
xxxxxxxxxx
Statistics for STL Creation
minimum: 1787; maximum: 1859; mean: 1824.66; std: 27.2845
Statistics for Array
minimum: 1830; maximum: 1910; mean: 1862.86; std: 27.3357
STL based creation is equivalent to native array creation. Here, it seems slightly faster, but that's likely an artifact of the state of my computer when running these tests. What about access? First, let's look at array access:
xxxxxxxxxx
evaluation_engine([&a_numbers]()
{
const auto value = a_numbers[3];
}, "Array Access");
Gives us these results:
xxxxxxxxxx
Statistics for Array Access
minimum: 2147; maximum: 2241; mean: 2160.32; std: 11.3568
Now std::array:
xxxxxxxxxx
evaluation_engine([&std_numbers]()
{
const auto value = std_numbers[3];
}, "STL Access");
xxxxxxxxxx
Statistics for STL Access
minimum: 3629; maximum: 3998; mean: 3760.79; std: 121.434
Using std::array::at(.)
xxxxxxxxxx
Statistics for STL at() Access
minimum: 4320; maximum: 5094; mean: 4684.56; std: 242.694
Now looking at std::array access:
xxxxxxxxxx
evaluation_engine([&std_numbers]()
{
const auto value = std_numbers.at(3);
}, "STL at() Access");
Now, I'm using the high-resolution clock in these measurements, and I'm not that interested in wall clock time. I am interested in comparative performance though. Based on these measurements, we can see that native array access is a bit over 40% slower than std::array access. Std::array::at(.) access is on the order of 20% slower than that.
On my system, these measures are in nanoseconds, so this is still pretty darn fast. Nevertheless, std::array use isn't free.
What about the assignment?
xxxxxxxxxx
Statistics for Array assignment
minimum: 2057; maximum: 2775; mean: 2205.31; std: 81.6836
Statistics for STL assignment
minimum: 3323; maximum: 3556; mean: 3403.62; std: 28.7593
Statistics for STL at() Assignment
minimum: 3640; maximum: 3882; mean: 3705.12; std: 42.9675
Again, a bit of a range, with std::array::at(.) being the slowest. Let's look at copying:
xxxxxxxxxx
Statistics for Array copy
minimum: 2091; maximum: 2156; mean: 2102.45; std: 11.4301
Statistics for STL copy
minimum: 2608; maximum: 2729; mean: 2626.83; std: 17.4178
Clearly, in all cases except initial creation, the native array is more performant than the std::array type. Keep in mind, in each of these cases, I'm performing 100,000 operations, and we're talking about a difference of a hair over two milliseconds between a native array and std::array time.
Which to use? Well, I'd suggest you use std::array in virtually all cases — at least, always start with it. You may need to transition to a native array in some cases, but frankly, you can usually squeeze more performance via algorithmic improvement than changing the data structure you use.
Further Reading
Two Lines of Code and Three C++17 Features: The Overload Pattern
Opinions expressed by DZone contributors are their own.
Comments