13 ms·
Static arrays are the best vectors
- jstimpfle 2y agoI share a dislike for std::vector type automatically reallocating arrays -- because of the "reallocating" part. I like preallocated chunks and stable pointers. To a degree, I agree with the premise of the post, we can preallocate huge chunks, and actual resource consumption will only happen on demand when the memory is first used. But note that there is still a resource whose allocation we can't delay: virtual address space, which must get acquired immediately obviously. When trying to be compatible with 32-bit machines (less than 4GB of virtual address space available), preallocating huge chunks is not practical. As to using static globals, I'm not sure it's the best idea. One can use mmap() (or Virtual Alloc() on Windows) to the same effect. That might be better from an architectural view, with regards to encapsulation and such, and allows a little more dynamicity, making multiple separate (but still large) allocations and such. It's possible to give debugger-visible names to these allocations as well, for example by way of the memfd_create() API.
- 082349872349872 2y agoSomewhere I ran across the idea that programs should distinguish between errors and apologies in their messaging. An error indicates the user's fault: they need to correct their input before retrying. An apology indicates the program's fault: either logic for what had been incorrectly thought to be a rare^2 case is missing, or the program has only been configured to deal with N foos, and the user's input requires more. In the former case, the user needs to get a programmer fix before retrying, but in the latter, maybe they can just reconfigure and rebuild (even better, restart with different env vars?) then retry. The relevant point of this is that when the user is likely to be another programmer, "reconfigure" can be as simple as editing a static allocation.
- jstimpfle 2y agoI like the idea of "apologies" -- sometimes the best thing a program can do is to try, and if it doesn't work then re-try, maybe with a small change guided by the user. However, many programs require a bit more planning how to deal with unfortunate situations, at least without entirely restarting. It's good to not prevent repairing an unfortunate situation from the start by baking in certain constants that can't be practically assumed. Better than editing a static allocation, is if the program can cope with that situation automatically in some way. Next better thing, have it runtime configurable (e.g. command line arguments, no recompilation needed). I don't see a huge benefit to prefer global arrays over memory maps generally.
- Deukhoofd 2y agoIsn't that basically the distinction in HTTP error codes? The 4xx range indicates the user did something wrong, the 5xx indicates the program did something wrong.
- wheybags 2y agoI made an std::vector like wrapper[1] based on this principle, it was fun. Also focused on some of the other advantages you get, like never having to move data when you grow the array, and not invalidating pointers/iterators. Never really used it though. 1: https://wheybags.com/blog/pinned.html https://wheybags.com/blog/pinned.html
- Veliladon 2y ago> I share a dislike for std::vector type automatically reallocating arrays -- because of the "reallocating" part. I like preallocated chunks and stable pointers. Use request at creation to set a capacity for 99.9% of use cases. Still have realloc there to stop any sneaky buffer overflows. Have your cake and eat it too!
- jstimpfle 2y agoYou can't just realloc you way out of your buffer overflow, when this will invalidate existing references to members in the old allocation.
- MontagFTB 2y agostd::deque doesn’t invalidate existing elements when new ones are added to either end of it. It’s not a panacea, and has its own set of tradeoffs, but if non-invalidation is a requirement for you it is available.
- ape4 2y agoIn that case if a realloc() happens won't it request double the existing size (ie 99.9% * 2) But if we had let the vector grow naturally it would probably not make such a big realloc()
- Joker_vD 2y agoThe annoying thing about "reallocating" is that it's mostly the price of having the flat address space (and not dealing with 128-wide fat pointers). The virtual memory subsystem already juggles around physical pages to allow two non-overlapping virtual address ranges to transparently map to overlapping physical address ranges. The implementations of std::deque() from C++ standard library usually use the similar technique but it's arguably a bit silly since it essentially pushes a MMU-like system on top of an existing hardware one!
- gizmo686 2y agoI don't think are current memory architecture is fundamental incompatible with a paging based vector. Doubling the size of pointers is expensive. However, given current RAM sizes, our existing 48-bit effective pointer size is already large enough. It isn't enough to give every allocation a massive amount of virtual address space to grow into. But it should be more than enough to seperate out actual memory allocation from virtual address space allocation. This would mean a more complicated malloc, where you ask for both an actual memory allocation, and an amount of address space to be allocated. And will still require you to have some sense of the maximum size you might need in a given vector. Also, it forces memory allocations to be multiples of the page size, which isn't always ideal.
- Joker_vD 2y agoWell, I am not exactly arguing with all of that but — have you heard of CHERI [0]? They are absolutely willing to double the size of the pointers. [0] https://www.cl.cam.ac.uk/research/security/ctsrd/cheri/cheri-publications.html https://www.cl.cam.ac.uk/research/security/ctsrd/cheri/cheri...
- pjmlp 2y agoThat is what std::array is for.
- on_the_train 2y agoI'd really want a vector variant that is still on the heap but init-allocated. That's most of the use cases for a std vector in my experience. It's trivial to write yourself of course, but non-reserved vector usage is a very common perf problem
- pjmlp 2y agoYou can wrap a std::array into a std:unique_ptr, hardly much to write besides a couple of type alias.
- jstimpfle 2y agoI'm sorry to say, TERRIBLE advice! As many (or most) of C++ features, std::array is a logical idea with terrible ergonomics, bordering to unusable. While they are potentially a little safer to use than plain C arrays (but compilers could easily add runtime checking for plain C arrays too), they are much less ergonomic, both in verbosity of declaration and errors. They can't replace an init-allocated std::vector either, because they have a size fixed at compile time. And unique_ptr itself is TERRIBLE to use too. If RAII is your thing, then still very often, it's totally worth the boilerplate to wrap individual objects in type-specific explicit "Holder classes", instead of relying on std::unique_ptr which has terrible ergonomics too, again both for declaration and use. Oh, and if you're using a free-standing destructor function for the managed object (which is often a very good idea because exposing the class definition in typical C++ style leads to TERRIBLE TERRIBLE compile times), you'll have to bake in a function pointer that is carried by unique_ptr at runtime (I suspect that is because they couldn't make the function a templated parameter because in C++, this leads to ambiguitiy if it is a static inline function declared in a header (terrible!)). So you're paying for all the all the drawbacks of the templated type and get none of the benefits (except for saving a few lines of boilerplate). TERRIBLE. And now you propose combining these two terrible things? How would you use this? #include <memory> #include <array> struct Foo { ~Foo() { printf("Bye!\n"); } }; int main() { // std::unique_ptr<std::array<Foo, 42>> foo = std::make_unique<std::array<Foo, 42>>(); // my fingers hurt and my eyes bleed auto foo = std::make_unique<std::array<Foo, 42>>(); // fingers hurt a little less but my brain will melt on next compiler error foo->at(0); // valid foo[0]; // error. TERRIBLE! return 0; } I wouldn't be so upset about your advice if I didn't know you often don't validate your claims and "references", but keep on suggesting and criticising existing practice that has evolved to certain points (and keeps evolving!) for a reason.
- flohofwoe 2y agoThis behaviour is platform specific unfortunately. For instance I'm pretty sure that Emscripten-compiled WASM running in browsers will fail if the array doesn't fit into the pre-allocated INITIAL_MEMORY size (even when ALLOW_MEMORY_GROWTH is enabled) - not sure about other WASM runtime environments though. Similar problems probably on embedded platforms without MMU.
- deleted 2y ago[deleted]
- runlaszlorun 2y agoI’ve dinkered around Wasm some but am certainly no expert. Were you referring to the initial memory that is allocated from the Wasm code and/or js, etc? Or is this something set by the Wasm runtime? I haven’t run it myself but I’ve heard others mention that you can prob get several hundred MB to a GB or so but shouldn’t count in getting near the 4GB limit. Nonetheless, that’s plenty for my use case and random hacking.
- flohofwoe 2y agoI'm not sure if it's a WASM requirement or specific to some WASM runtimes or compilers. At least in Emscripten you have an INITIAL_MEMORY linker option which describes the initial size of the WASM heap. The program, its static data and the stack need to fit into this initial size. From that point on, any dynamically allocated data is fine because that will grow the WASM heap as needed (up to 4 GB for wasm32).
- cesaref 2y agoYum, global variables to avoid a call to malloc/realloc. This is not progress.
- flohofwoe 2y agoMutable global state isn't a problem, only shared mutable global state may be (for instance when the global state is writable from unexpected places in the code base or from other threads). There's a lot of problems that can be solved just fine with an upfront defined fixed memory layout in a global static variable. Dismissing globals is basically the same problem as sweeping generalizations like "goto considered harmful", "avoid for-loops" or "almost always auto" - such statements can serve as advice for newbies, but following the advice blindly can also lead to less elegant and less maintainable solutions.
- adrianN 2y agoGlobal state is not a problem until your program becomes larger than a few screens of code.
- LegionMammal978 2y ago> (for instance when the global state is writable from unexpected places in the code base or from other threads) ...or from a function that calls itself, directly or indirectly. This automatically becomes possible basically whenever the function accesses a user-supplied callback. So you'll run into issues unless you have full control over your callees, or only access the global array in logically-atomic patterns. (To give an ancient example: to enable function calls, PDP-8 programs allocated a word of static memory before the top of each subroutine to store the return address. However, if a subroutine tried to recurse into itself, then the old return address would be overwritten, and it would never be able to return properly. Supposedly this could result in very gnarly errors to debug.)
- Slyfox33 2y agoGlobal mutable state is still terrible design unless actually necessary. Any function at all in your entire program can change the state of the thing which makes it very difficult to debug and reason about.
- EdSchouten 2y agoSo you can only declare a couple of hundred lists, and you’ve exhausted your process’s 2^47 byte virtual address space. Great.
- smcameron 2y ago> If you make the array extremely large, your program will still compile just fine. It may compile, but it might not link. With very large static arrays, in the link step, you might get something like this: my_program.o: In function `my_function': my_program.c:(.text+0xf38): relocation truncated to fit: R_X86_64_PC32 against `.bss' It's due to the linker assuming statically allocated things can be addressed with a 4-byte pointer (I encountered this on x86_64 arch). Using malloc to allocate the array will fix this. That being said, using malloc once to allocate a very large array will get the same dynamic allocation of backing RAM as described in the article on linux, assuming overcommit is enabled.
- api 2y agoAFAIK just about all Unix-like OSes overcommit and dynamically allocate like this. I know macOS and xBSD (which which macOS is one) do this. I don't know what Windows does.
- simiones 2y agoI don't know about other Unixes, but Linux allows the sysadmin to decide if overcommit is allowed or not. I believe Windows does as well, but the default is to not overcommit.
- bakugo 2y ago> I don't know what Windows does. Windows does not allow overcommit. The commit limit is the total size of RAM + the size of the page file, attempting to commit more than that will fail.
- bregma 2y agoThat's where -fPIC (as opposed to -fpic) comes in.
- tetromino_ 2y agoThe main disadvantage of this approach is that to free memory, you must terminate the program. This is fine for a program which runs for a second, but is unusable for long-running processes and servers where you want to free memory after finishing any memory-intensive task so that everything else that your machine is running concurrently gets memory too.
- mananaysiempre 2y agomadvise(MADV_FREE) (on *BSD) or madvise(MADV_DONTNEED) (on Linux) will remap the specified memory range to the zero page or leave it intact at the kernel’s discretion.
- account42 2y agoCongratulations, you have reinvented realloc and are no longer using simple static arrays.
- mananaysiempre 2y agoUnlike realloc, though, I’m keeping element addresses stable. (I’m actually somewhat conflicted about TFA’s idea, for what it’s worth, I just don’t think freeing memory is the most problematic part of it.)
- blueflow 2y agoOr have it evicted to swap. The kernel is pretty good at managing it automatically.
- dataf3l 2y agoI believe the plural form of person is people.
- nathan_compton 2y ago"People should always be used when a collective noun referring to the entirety of a group or nation (i.e., "the French People") is called for. For references to groups of a specific or general number, either people or persons may be used. However, modern style guides tend to prefer people where earlier guides preferred persons, especially for countable groups. " https://www.merriam-webster.com/grammar/people-vs-persons https://www.merriam-webster.com/grammar/people-vs-persons Then again, I'm not a language prescriptivist. I post the above only to demonstrate that the use of "persons" is hardly unusual.
- RcouF1uZ4gsC 2y agoAnd did you know that if you put your entire program in the main function you can use labels and goto to have something like exceptions and coroutines.
- dvfjsdhgfv 2y agoI feel the use of "vectors" and "arrays" together makes it a bit confusing but I guess the title "Static arrays are the best dynamic arrays" would be even more confusing...
- meindnoch 2y agoGenius. But I have an even better lifehack: if you compute the result of your program locally, you can replace the whole program with a single call to printf()!
- nathan_compton 2y agoTruly deranged.
- munchler 2y agoCongratulations, you have reinvented Fortran 77!
- rhelz 2y agoZonk. This is a very astute observation. Its awesome behavior, but its good by accident and luck. Therefore... Programming languages should directly support a data structure which is guaranteed to have this behavior. Not just that it allocates a new array which is 4k bigger and the copies it over. But one which grows by having the OS map another 4k block to have the addresses past the old end of the array.
- binary132 2y agoIsn’t this implementation-defined?
- ay 2y agoLooks cute on the surface, but if the program ever has to dump a core, it will eat up a fair bit of space, so RCA will be a massive pain. I made a small experiment with 1-million int array, which has a single element written to at index 100000, and then force a core dump by writing to a dereferenced int pointer which is initialized as 0. The resulting core file is almost 4gigabytes.