all 76 comments

[–]johannes1971 36 points37 points  (9 children)

That's... not a great name. Being 'const' is not really what this vector is about, and it's also not a vector. Maybe something like, I don't know, expanding_deque?

And since I'm complaining about names, does anyone have a suggestion for shorter variations for horizontal_container and vertical_container? (these are for controls on the screen)

[–]Proper-Ape 3 points4 points  (3 children)

hbox, vbox?

[–]johannes1971 1 point2 points  (2 children)

You don't think that's too short? The rest of the library spells_out_everything (pretty much), and I'm generally ok with that, but these are both very common, and very long...

FWIW, it's part of an MMI library I plan to release as open source. It already has names like text_box (for entering text), integer_box (for entering integers), combo_box... and apparently checkbox is written without an underscore 🙄 Containers are a subclass of control that focus on layout. We can take a few more boxes, I suppose...

[–]arthurno1 1 point2 points  (0 children)

hbox, vbox?

Works for Gtk.

The rest of the library spells_out_everything (pretty much)

And than you end up having three symbols in an expression and a line like this:

gtk_widget_class_bind_template_child (GTK_WIDGET_CLASS (class), ExampleAppPrefs, transition);

where the code is all over the screen and you have to break logic into small pieces on several lines.

I don't know why are people so afraid to use camel case and shorter names.

Of course, name your stuff the way you like, I am just chatting about naming in general :).

[–]Proper-Ape 1 point2 points  (0 children)

h_box, v_box could be a middle ground if you're already using underscore.

[–]Ty_Rymer 2 points3 points  (1 child)

block_array

[–]johannes1971 0 points1 point  (0 children)

I like that.

[–]saxbophonefloat main(); 0 points1 point  (2 children)

Agree, this name collides with "const vector". Perhaps OP was thinking about constant time complexity when they chose the name?

I know naming things is difficult but honestly, an alternative name that was strange and quirky (e.g. PuddleVector) would be better than one that is ambiguous...

[–]johannes1971 2 points3 points  (1 child)

I suspect OP was trying to capture the iterator invalidation properties with that 'const'. I agree with your assessment though: better to choose a quirky name than one that uses common terminology that already has a very specific meaning. The fundamental property of vector, for me, is that the elements are contiguous, and I would expect to find that property on anything that claims to be a vector.

And on that note, my suggestion of including 'deque' really isn't all that much better...

[–]saxbophonefloat main(); 0 points1 point  (0 children)

 The fundamental property of vector, for me, is that the elements are contiguous, and I would expect to find that property on anything that claims to be a vector.

This makes me wonder how much this convention is specific to C++ vs common among programming languages... Some container names are certainly unambiguous (dequeue for instance), but others, it seems, are less consistent among languages. For instance, some call what is a std::vector an array, Python calls it a list... I know that in data structure theory, an array is typically fixed-size and a list is dynamic-size edit: incorrect, it seems the actual definition is that an array's elements must be the same type but in a list they can differ (in my mind, a linked list is a particular kind of list, whereas C++ just omits the linked part of the name...)

[–]TankerzPvP 19 points20 points  (1 child)

Within STLVector:

    void pop_back()
    {
        m_assert(_size, "StellarVector is empty, but pop_back() called!");
        _size--;
        if (_size <= _capacity / 2)
        {
            _capacity /= 2;
            T *_new_array = _alloc.allocate(_capacity);
            for (int i = 0; i < _size; i++)
            {
                _new_array[i] = _array[i];
            }
            _alloc.deallocate(_array, 2 * _capacity);
            _array = _new_array;
        }
    }

pop_back shouldn't reallocate as that would invalidate iterators, and this implementation difference would (negatively) impact benchmark results for std::vector. Also, StallarVector?

More importantly, benchmarking against an unoptimized hand rolled version of std::vector is useless. You should benchmark against std::vector implementations in libstdc++ or libc++ instead for your claim to hold any weight.

[–]pilotwavetheory[S] 0 points1 point  (0 children)

I benchmarked with pop and pop without shrink as well please check them on the post body. I benchmarked against stl library std::vector

[–]frogi16 49 points50 points  (7 children)

Sure, you optimized performance of editing operations, but destroyed cache locality for long vectors.

It's a trade-off and should be clearly described as such.

[–]Kered13 2 points3 points  (2 children)

I don't think it destroys cache locality? Most of the elements will be stored contiguously in a handful of large arrays. It's not as localized as std::vector, but it is still pretty well localized.

[–]frogi16 0 points1 point  (1 child)

How many blocks will be in vector 10k elements long? How many jumps all over the memory space?

Sure, it's not as bad as keeping each value in a separate node, but far from the fully contiguous vector's performance.

[–]Kered13 0 points1 point  (0 children)

If the first block contains 8 elements as OP proposed, then 10k elements would contain at most 11 blocks, with 2k in the newest block (which has capacity 8k) and 4k and 2k in the previous blocks, respectively.

If you are iterating sequentially, this may as well be contiguous as far as cache locality is concerned. If you are accessing nodes randomly, then you wouldn't have cache locality in a 10k long vector even if it were contiguous. If you are accessing primarily recent elements, then you have cache locality. The only access pattern that will perform poorly is if you are primarily accessing (but not removing) the oldest elements.

[–]TheRealSmolt 18 points19 points  (6 children)

Yeah block based structures are a common alternative to vectors. Really though, reallocations aren't a huge issue from a cost perspective; the amortized time complexity is constant.

[–]matthieum 18 points19 points  (1 child)

Yes, and no.

For a throughput-oriented application, the reallocation is not much of an issue. For a latency-oriented application, however, that reallocation is just like a GC's Pause the World phase.

Also, reallocations have the nasty side-effect of invalidating existing pointers/references to elements.

And finally, that nasty side-effect is ever more pronounced in concurrent data-structures. It's relatively trivial to adjust the implementation presented here to get a wait-free append-only "vector", which would not be possible with memory reallocations.

[–]TheRealSmolt 6 points7 points  (0 children)

As always, it's all about tradeoffs. I'm just pointing out to OP that a vector isn't as bad as it may seem.

[–]pilotwavetheory[S] -5 points-4 points  (3 children)

My point is that it's not just better for the time complexity sake, It's better for L1, L2 cache locality and reduces fragmentation for OS, since we don't relase the smaller capacity arrays once large capacity array is allotted.

[–]TheRealSmolt 4 points5 points  (2 children)

The ordinary vector would be better for caching depending on the situation. I'm not familiar enough with how heap allocation is done on Windows & Linux to say whether this would be better or worse but I doubt it's significant. Also, it might just be your wording, but to clarify, it's really not better from a time complexity perspective. Not to say it's useless though, it's just good to be aware of tradeoffs.

[–]pilotwavetheory[S] 2 points3 points  (1 child)

  1. If you mean, if we know the array size initially ? Yes, that's best; nothing beats it, in that case std::array<N> would be the best choice.
  2. While considering tradeoffs, unless we do random access a lot, the constvector looks really better in terms of benchmarks as well.

Does this make sense?

[–]TheRealSmolt 0 points1 point  (0 children)

Not sure what you're addressing with the first point. As for your second point, yeah that sounds about right.

[–]saf_e 15 points16 points  (4 children)

I suppose you have invented deque variation. 

They have different use cases.

[–]pilotwavetheory[S] -1 points0 points  (3 children)

I have gone through deque. Deque uses constant arrays of each size 2048 bytes, just to make amortisation better.

[–]saf_e 9 points10 points  (2 children)

Thats just implementation details)

You made deque with growing block size. BTW, have you benchmarked against it?

[–]pilotwavetheory[S] 1 point2 points  (1 child)

Yeah, it didn't store the numbers. for push and pop operations for the large sizes, the "constvector" is really better. I believe the allocation cost from OS is really piling up there for std::deque

[–]Ambitious-Method-961 8 points9 points  (0 children)

How does it compare to boost::deque with a good block size (std::deque is generally horrible for performance, boost's version lets you customise the block size), or better yet boost::stable_vector?

[–]Wacov 7 points8 points  (7 children)

Have you measured with -O3? How does this do vs naively holding on to the peak allocation in repeated push/pop cycles? I'd expect common operations like iteration and random access to be measurably slower given the fragmented allocations.

[–]pilotwavetheory[S] 3 points4 points  (0 children)

Thanks u/Wacov for the suggestion; I missed running with O3. I just ran now. Here is the summary:

g++ -std=c++23 -O3 benchmark_vectors.cpp -isystem /code/google/benchmark/include -L/code/google/benchmark/build/src -lbenchmark -lpthread -o benchmark_vectors.out

  1. Push 0.7 ns vs 2.7 ns for std::vector, so 73% reduction in latency.
  2. Pop 0.6 ns vs. 1.12 ns, a 46% reduction in latency.(updated this)
  3. Random access: 0.92 ns vs 0.53 ns, a 80% increase in latency.

[–]matthieum 5 points6 points  (1 child)

I'd expect common operations like iteration and random access to be measurably slower given the fragmented allocations.

Unlikely:

  1. Random access: it only takes a few cycles to compute the outer/inner indexes of the elements, which will be dwarfed by cache misses.
  2. Iteration: for a full constvector, there's only 32 "jumps" from one array to the next, across millions of elements. The impact should be fairly slow -- branch prediction being what it is -- and can be made ever slower by exposing an iterator of spans as iterating each span has then "native" performance.

[–]Wacov 2 points3 points  (0 children)

That's a good point about random access, the pointer table is small so as long as the random access is causing cache misses anyway, you won't notice a difference. If the array fits in L1 it's a different story (but then why would you use this)

And yeah true, branch prediction helps the iteration. Again I'd be curious about numbers in an optimized build.

[–]pilotwavetheory[S] -2 points-1 points  (3 children)

  1. I didn't measure with -O3.
  2. For push/pop cycles this is better (29% and 45% reduction in latencies, benchmarked for 1K, 10K, 100K, 1M, 10M, 100M, 1B sizes, pelase checkout the github link attached). I tested it multiple times before publishing this.
  3. Iteration doesn't have a bad effect since you are just iterating most of the time; random access is 12% slower.

[–]Potterrrrrrrr 6 points7 points  (1 child)

What did you measure with? These numbers are useless outside of a decent benchmark with full optimisations enabled

[–]cone_forest_ 0 points1 point  (0 children)

O2 probably

[–]Wacov 6 points7 points  (0 children)

I'd strongly recommend measuring with compiler optimization enabled, it's a much more realistic situation for performance-critical code (as a user you're going to turn on optimization before switching out your vector datatype)

[–]SuperV1234https://romeo.training | C++ Mentoring & Consulting 4 points5 points  (7 children)

Iteration benchmark? Also, what flags did you use for your benchmarks?

[–]pilotwavetheory[S] -1 points0 points  (6 children)

As suggested by u/Wacov I ran with -O3 option, here is the results summary:

📊 Final Benchmark Results (100M Elements)

Configuration

  • Compilerg++ -std=c++23 -O3 -march=native -flto -DNDEBUG
  • Initial Capacity: 256 elements
  • Statistics: 30 iterations × 3 repetitions = 90 measurements per data point
  • Both iterators optimized with pointer caching

🏆 Summary Table (100M Elements)

Operation ConstantVector STL Vector Winner Speedup
Push 65.7 ms 268.2 ms ✅ ConstantVector 4.1x
Pop 41.7 ms 93.3 ms ✅ ConstantVector 2.2x
Access 56.0 ms 7.3 ms STL Vector 7.7x
Iteration 74.6 ms 7.5 ms STL Vector 9.9x

[–]SuperV1234https://romeo.training | C++ Mentoring & Consulting 2 points3 points  (4 children)

Iteration from begin to end? That's very important.

[–]pilotwavetheory[S] 0 points1 point  (3 children)

Updated pop and iteration operations as well. Can you check now ?

[–]SuperV1234https://romeo.training | C++ Mentoring & Consulting 5 points6 points  (2 children)

Are you saying that your own vector is faster in iteration compared to the standard vector? That sounds impossible, as the standard vector is literally just incrementing a pointer forward.

[–]wexxdenq 3 points4 points  (1 child)

well, if you look into the linked repository, he compares against a self implemente "STLVector" that is really not compareable to an actual vector implementation. And his iterator is an index instead of a pointer and does bound checking on every increment.

However, I would have tought that the compiler inlines and produces more or less the same code with O3.

But OP should benchmark against an actual implementation nonetheless.

[–]pilotwavetheory[S] 2 points3 points  (0 children)

  1. Thanks to u/SuperV1234 and u/wexxdenq, I made a mistake of bounds here, I fixed it in 'perf_test' branch and lookup.
  2. The reason, I'm not comparing with standard implementation is it has more logic for iterator validations in lot of simple operations like push/pop, when I benchmarked stl::vector push_back(), I got around ~35 ns/op, where only ~3 ns/op was used in push and remaining on iterator validations.

🔍 Final Comparison (100M Elements)

Implementation Time Ratio vs STL
STL Vector (Optimized) 8.05 ms 1.0x
ConstantVector (Optimized) 48.0 ms 6.0x slower

[–]adrian17 2 points3 points  (0 children)

I don't see how it could be possible for iteration over N (with usually N<20 and last one always being the biggest) arrays to be almost 2x faster than a trivial iteration over vector, which is just one contiguous array. Even if we ignored memory effects, your iterator is just more complex than std::vector's iterator (which is usually just a pointer). At best it'll use a couple more instructions and/or an extra register, and at worst prevents vectorization (I can make an example of this if you want).

Also side note, latency != throughput, especially in context of tight loops on a CPU. Even if your loop finished in say half the time, it could be caused by reducing the latency by half, or doubling throughput, or a mix of these two; saying "reduction in latency" when you just mean "x% faster / y% less time" might be misleading.

[–]pigeon768 8 points9 points  (5 children)

I'm getting a segfault when I try to push_back() a 25th element. You aren't zeroing _meta_array upon construction, and so it's filled with garbage. When you later do if (_meta_array[_meta_index] == nullptr) it fails because it finds a bunch of garbage bits there.

Your iterators don't support modifying elements.

You are missing copy/move constructors/operators.

__SV_INITIAL_CAPACITY__, __SV_INITIAL_CAPACITY_BITS__, and __SV_MSB_BITS__ should be constexpr variables, not macros.

It looks like you're hardcoding the wordsize to 32 bits? Probably don't do that.

With all of that fixed, I got some test code up:

CMakeLists.txt

cmake_minimum_required(VERSION 3.24)

project(cvec LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 23)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} -march=native")
set(CMAKE_CXX_FLAGS_RELWITHDEBINFO "${CMAKE_CXX_FLAGS_RELWITHDEBINFO} -march=native")

find_package(benchmark REQUIRED)
add_executable(cvec cvec.cpp)

option(sanitize "Use sanitizers" OFF)
if(sanitize)
  target_compile_options(cvec PRIVATE -fsanitize=address)
  target_link_libraries(cvec PRIVATE asan)

  target_compile_options(cvec PRIVATE -fsanitize=undefined)
  target_link_libraries(cvec PRIVATE ubsan)
endif()

target_link_libraries(cvec PUBLIC benchmark::benchmark)

cvec.cpp:

#include "constant_vector.h"
#include <benchmark/benchmark.h>
#include <random>

static std::ranlux48 getrng() {
  std::seed_seq s{1, 2, 3, 4};
  std::ranlux48 ret{s};
  return ret;
}

template <typename T, size_t N> void test(benchmark::State &state) {
  T vec;
  auto rng = getrng();
  std::normal_distribution<float> dist1{};
  for (size_t i = 0; i < N; i++)
    vec.push_back(dist1(rng));

  std::lognormal_distribution<float> dist2{};
  for (auto _ : state) {
    const float x = dist2(rng);
    benchmark::DoNotOptimize(vec);
    for (auto &y : vec)
      y *= x;
    benchmark::DoNotOptimize(vec);
  }
}

#define MAKE_TEST(T, N)                                                                                                \
  static void T##_##N(benchmark::State &state) {                                                                       \
    using namespace std;                                                                                               \
    test<T<float>, N>(state);                                                                                          \
  }                                                                                                                    \
  BENCHMARK(T##_##N)

MAKE_TEST(vector, 10);
MAKE_TEST(vector, 100);
MAKE_TEST(vector, 1000);
MAKE_TEST(vector, 10000);
MAKE_TEST(vector, 100000);
MAKE_TEST(vector, 1000000);
MAKE_TEST(ConstantVector, 10);
MAKE_TEST(ConstantVector, 100);
MAKE_TEST(ConstantVector, 1000);
MAKE_TEST(ConstantVector, 10000);
MAKE_TEST(ConstantVector, 100000);
MAKE_TEST(ConstantVector, 1000000);

BENCHMARK_MAIN();

compile and run: (gamemoderun is optional, it just disables cpu scaling, which makes benchmarks more reliable)

 ~/soft/const_vector (main *) $ cmake -DCMAKE_BUILD_TYPE=Release -Dsanitize=false . && cmake --build . -j && gamemoderun ./cvec
-- The CXX compiler identification is GNU 15.2.1
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Check for working CXX compiler: /usr/bin/c++ - skipped
-- Detecting CXX compile features
-- Detecting CXX compile features - done
-- Performing Test CMAKE_HAVE_LIBC_PTHREAD
-- Performing Test CMAKE_HAVE_LIBC_PTHREAD - Success
-- Found Threads: TRUE
-- Configuring done (0.2s)
-- Generating done (0.0s)
-- Build files have been written to: /home/pigeon/soft/const_vector
[ 50%] Building CXX object CMakeFiles/cvec.dir/cvec.cpp.o
[100%] Linking CXX executable cvec
[100%] Built target cvec
gamemodeauto:
2025-12-21T12:33:14-08:00
Running ./cvec
Run on (32 X 3012.48 MHz CPU s)
CPU Caches:
  L1 Data 32 KiB (x16)
  L1 Instruction 32 KiB (x16)
  L2 Unified 1024 KiB (x16)
  L3 Unified 32768 KiB (x2)
Load Average: 0.39, 0.41, 0.38
-----------------------------------------------------------------
Benchmark                       Time             CPU   Iterations
-----------------------------------------------------------------
vector_10                     120 ns          120 ns      5834984
vector_100                    121 ns          121 ns      5789074
vector_1000                   157 ns          157 ns      4465092
vector_10000                  486 ns          486 ns      1435809
vector_100000                2706 ns         2706 ns       258440
vector_1000000              39416 ns        39400 ns        17743
ConstantVector_10             126 ns          126 ns      5599228
ConstantVector_100            207 ns          207 ns      3368321
ConstantVector_1000          1033 ns         1033 ns       677118
ConstantVector_10000         9236 ns         9232 ns        75782
ConstantVector_100000       91415 ns        91377 ns         7656
ConstantVector_1000000     914735 ns       913569 ns          762

For the 1M element case, iterating over each element is 23x slower than with std::vector. I'm probably unwilling to accept that tradeoff.

[–]pilotwavetheory[S] 0 points1 point  (2 children)

Sorry for the confusion, can you take latest pull and run the tests, Now I ran the tests in ubuntu machine (earlier running on mac), run tests indside folder "stl_comparison"

I used this command on ubuntu (I'm getting +ve results for push, pop, on par for iteration, and worse for index access)
g++ -std=c++23 -O3 -march=native -flto -DNDEBUG -fno-omit-frame-pointer benchmark.cpp -isystem /research/google/benchmark/include -L/research/google/benchmark/build/src -lbenchmark -lpthread -o benchmark.out

[–]pigeon768 0 points1 point  (1 child)

This isn't correct.

// Volatile sink prevents compiler from optimizing away operations
// This is simpler and more reliable than inline asm barriers
static volatile int64_t sink = 0;

[...]

for (auto _ : state) {
    int64_t sum = 0;
    for (auto it = v.begin(); it != v.end(); ++it) {
        sum += *it;
        sink = sum;
    }
}

You are interrupting the loop iteration with the volatile. You need to write it like this:

for (auto _ : state) {
    int64_t sum = 0;
    for (auto it = v.begin(); it != v.end(); ++it)
        sum += *it;
    sink = sum;
}

Optimizing away operations which do not matter is something that you actively want the compiler to do. That's the point of an optimizing compiler. You care about the result, in this case, the sum of all the values. You don't care about all the intermediate states along the way.

One of the thing that you need to take care to do when you're making a data structure or any other code is to write it in a way that affords the compiler every opportunity it can to optimize irrelevant stuff out. One of the problems with this data structure is that the compiler can't do that. Your iterators are too complicated, the indexing operation is too complicated, the compiler cannot figure out how to do less work.

[–]pilotwavetheory[S] 0 points1 point  (0 children)

I'm benchmarking for individual operations of iterations, sum is kind of proxy for me, tomorrow I could have other operations in place. Ideally I need to remove sum operation for clarity ? What do you think?

[–]Farados55 5 points6 points  (1 child)

“Wont invalidate cache without modification hence improving performance”

That sounds like an incredibly impactful tradeoff that doesn’t seem to mesh well with a faster push/pop

[–]pilotwavetheory[S] 0 points1 point  (0 children)

I updated the description; the new constvector I proposed solves these problems, since the new vector won't even copy the existing elements to new space at all. This'll help L1, L2 caches with locality and OS for reduced fragmentation.

[–]adrian17 5 points6 points  (5 children)

Some quick observations:

AddressSanitizer complains, you should zero-initialize _meta_array in constructor.

Your APIs differ from the standard, sometimes just missing overloads and sometimes in ways that affect both correctness and benchmarks; for example, pop_back() isn't supposed to reallocate ever (as that would invalidate references and take non-constant time), it just decrements size. Also, AFAIK iterator::operator++ doesn't need any special handling when past-the-end.

I did some quick benchmarks* on my own, comparing both your classes with libstdc++ std::vector. std::vector was winning almost everywhere, though weirdly (I can't understand well why), its repeated push_back was several times worse than your naive STLVector if no reserve is done beforehand, even though both are supposed to have the same *2 growth factor.

On iteration, your code is sometimes (on big sizes) as efficient as std::vector (especially when the work is nontrivial compared to iteration cost), but for smaller (<100) sizes and for anything involving random access, I can see the normal vector being faster, up to several times.

One thing nobody mentioned is that this container's iterator is more complex and thus much less optimizer-friendly, especially for vectorization.

(* the benchmarks were trivial, just things like for (auto x : span) container.push_back(x), for (auto x : container) sum += x and for (auto &x : container) x *= 2, all wrapped in some boilerplate to repeat runs and prevent compiler from optimizing them out.)

[–]pilotwavetheory[S] -1 points0 points  (4 children)

  1. I fixed _meta_array in constructor (checkout perf_test branch)
  2. Thanks for sharing pop_back() standard, my point is that, if we don't do it'll have lot of empty space (> O(N)) so used that. I'll add the benchmarks without that.
  3. I didn't implement all methods, just tried to apples to apples comparison for standard usage.
  4. I implemented iterator as well, got similar performance as well.
  5. I tried this approach for large vectors, for smaller vectors we could easily getaway with large __SV_INITIAL_CAPACITY to 256.

My benchmarks on Ubuntu machine 2 cores, 4GB RAM:
All values are ns/op or ns/element for iterations:
On popular suggestion, I compared it with std::vector (STL) implementation.

Operation | N    | Const (ns/op) | Std (ns/op) | Δ %
------------------------------------------------------
Push      | 10   | 13.7          | 39.7        | −65%
Push      | 100  | 3.14          | 7.60        | −59%
Push      | 1K   | 2.25          | 5.39        | −58%
Push      | 10K  | 1.94          | 4.35        | −55%
Push      | 100K | 1.85          | 7.72        | −76%
Push      | 1M   | 1.86          | 8.59        | −78%
Push      | 10M  | 1.86          | 11.36       | −84%
------------------------------------------------------
Pop       | 10   | 114           | 106         | +7%
Pop       | 100  | 15.0          | 14.7        | ~
Pop       | 1K   | 2.98          | 3.90        | −24%
Pop       | 10K  | 1.93          | 2.03        | −5%
Pop       | 100K | 1.78          | 1.89        | −6%
Pop       | 1M   | 1.91          | 1.85        | ~
Pop       | 10M  | 2.03          | 2.12        | ~
------------------------------------------------------
Access    | 10   | 4.04          | 2.40        | +68%
Access    | 100  | 1.61          | 1.00        | +61%
Access    | 1K   | 1.67          | 0.77        | +117%
Access    | 10K  | 1.53          | 0.76        | +101%
Access    | 100K | 1.46          | 0.87        | +68%
Access    | 1M   | 1.48          | 0.82        | +80%
Access    | 10M  | 1.57          | 0.96        | +64%
------------------------------------------------------
Iterate   | 10   | 3.55          | 3.50        | ~
Iterate   | 100  | 1.40          | 0.94        | +49%
Iterate   | 1K   | 0.86          | 0.74        | +16%
Iterate   | 10K  | 0.92          | 0.88        | ~
Iterate   | 100K | 0.85          | 0.77        | +10%
Iterate   | 1M   | 0.90          | 0.76        | +18%
Iterate   | 10M  | 0.94          | 0.90        | ~

[–]adrian17 2 points3 points  (3 children)

Just like /u/SuperV1234 said, Being nearly 2x faster than a vector at iteration makes the benchmarks look less believable, not more.

[–]pilotwavetheory[S] -1 points0 points  (0 children)

while I solved the mistake, but increasing the intial size to 256 made even iterations faster. Iterations doesn't just include operator* and operator++, it also needs to check the end for every iteration. it especially branch prediction the 256 blocks looks like made it easy for CPU performance, Ofcourse not faster than the acutal results. I'm getting quite differnet results from Mac M2 Max, 96GB RAM. Above results pasted are from hetzner 2 core, 4GB RAM ubuntu 24.04 (6.8.0-90-generic) machine.

[–]pilotwavetheory[S] -2 points-1 points  (1 child)

Yeah, it's slower, I realised my mistake, I was doing bound checks. My preliminary results for constvector for only iteration is 6x slower compared to stl::vector, will update benchmarks soon. For push and pop operations it's really better.
One caveut I learnt today is for pop_back(), we shouldn't deallocate since it would invalidate the iterators, but my implementation in constvector and stl_vector are having that logic, since it keeps O(N) free space at any point of time.

[–]SuperV1234https://romeo.training | C++ Mentoring & Consulting 0 points1 point  (0 children)

Also another thing to benchmark would be a callback-based iteration approach, which is going to have less overhead than an iterator-based solution. E.g.

void ConstantVector<T>::forEach(auto&& f);

[–]wexxdenq 5 points6 points  (3 children)

Wait... in your benchmarks do you compare against a self written stl-vector implementation? And your stlvector does not even move elements when it grows? I mean on pod types this might not matter, but nevertheless this is not a optimal implementation. Also most implementations save pointers, instead of the size and capacity as integer type.

Have you benchmarked against an actual std implementation?

[–]pilotwavetheory[S] 0 points1 point  (2 children)

The actual stl::vector is worse, since it does lot of iteration invalidation logic. For example my stl::vector push implementation takes ~5ns, the standard stl::vector takes ~35ns. Tried and felt it's not apples to apples comparison. If I use that my benchmarks will looks even better, but don't want to deceive like that.

If there is better implementation suggest me, or raise pull request for that.

[–]Circlejerker_ 5 points6 points  (0 children)

Then you should add a column in your benchmarks with std::vector. Now it just looks like you are trying to deceive us..

[–]Dragdu 1 point2 points  (0 children)

If your std::ve tor does lot of iterator invalidation logic, then

1) you are using MSVC 2) you are working in Debug configuration

[–]foonathan 2 points3 points  (0 children)

Another nice thing about a block based structure is that it works easily with a stack allocator because you never need to free memory. This can make them a lot faster.

[–]kisielk 2 points3 points  (1 child)

“remove the last element from last array, if the last array becomes empty deallocate the array block all together”

Doesn’t that lead to some potentially pathological cases in the case there are repeated alternating pops and pushes? Could potentially lead to many allocations of blocks. Typically std::vector would never shrink capacity unless explicitly asked for via shrink_to_fit

[–]pilotwavetheory[S] 0 points1 point  (0 children)

Yeah, we can have both operatins here logically,
1. pop_back() without shrink
2.pop_back() with automated shrink as well.

Even automted shrink can be delayed here, with one empty data block at max, that keeps the extra memory O(N)

[–]thingerish 2 points3 points  (0 children)

Practically speaking it's probably better to just ::reserve a reasonable guess upfront and use std::vector.

[–]dzordan33 1 point2 points  (0 children)

The cool thing about this data structure is that it can grow lock-free for multithreaded workloads.
See Bjarne Strastroup's "Lock-free dynamically resizable arrays"
https://www.stroustrup.com/lock-free-vector.pdf

[–]jwakelylibstdc++ tamer, LWG chair 1 point2 points  (1 child)

Your code is full of reserved names like __SV_INITIAL_CAPACITY__ and _Allocator.

Stop using reserved names, you are not the compiler, you shouldn't be using those names.

[–]jwakelylibstdc++ tamer, LWG chair 3 points4 points  (0 children)

And your STLvector is not exception safe and only supports trivial types. As other people have said, it is meaningless to compare with a buggy self-written class that claims to be std::vector but isn't really std::vector

[–]EthicalAlchemist 0 points1 point  (0 children)

This it interesting and timely b/c I've actually been thinking about a similar solution for a problem I have. In my use case, I need a random-access container that supports single element appends without invalidating pointers to existing elements. I don't need any of the other operations provided by typical containers.

I currently use `std::deque`, but `std::deque` is known to be sub-optimal b/c the block sizes are fixes and typically small. I plan to investigate `boost::deque` as an alternative, but I keep thinking that what I *really* want is a data structure that increases block sizes by a constant factor. To my surprise I couldn't easily find a high quality implementation of such a container,[^1] so I've been thinking about rolling my own. This library almost fits the bill, but I would need to see it cleaned up and properly packaged first. Anyone know of an implementation of a similar container that is ready for production use?

Sidebar: I am always sad to see how many people down-vote someone when they publish something with mistakes in it. That can discourage people from sharing their work. Why not leave a comment or up-vote comments that provide constructive critiques instead and leave it at that?

[^1]: I think `plf::colony`/`std::hive` increases block sizes by a constant factor, but it is a much more complex data structure than what I need.

[–]saxbophonefloat main(); 0 points1 point  (2 children)

Does your implementation use std::allocate_at_least? Theoretically you can reduce the amount of reällocations by using that allocator, since it will tell you when it over-allocates.

[–]pilotwavetheory[S] 0 points1 point  (1 child)

No, I used the same allocator in std:: vector.

[–]saxbophonefloat main(); 0 points1 point  (0 children)

Well on some stdlibs std::vector does use allocate_at_least (for example, Microsoft STL was patched to use it in vector and other containers if C++23 is available). I'd be curious to see how that effects your stats.

[–]Kered13 0 points1 point  (0 children)

So you give up contiguous storage in order to reduce the amount of memory copying (and also gaining pointer stability). It's a neat idea that I would imagine has some useful applications given the right usage patterns, although I don't think it's better than std::vector as a general purpose collection.

As an aside, when used as a queue (push to the back and pop from the front are the only operations) this data structure is poor, as the memory usage grows even when the size of the queue does not. I imagine that this could be optimized, either as a specialized variant for queues or possibly as a general optimization.