What is the use of dynamic memory allocation in C language - Page 1
Forum section: electronics / general

author: Dadu@
author_rank: Contributor
timestamp: February 12, 2022, 12:02:19 pm
content: Can someone demonstrate with example what is the use of dynamic memory in C language?I tried a lot to understand it but I don't understand what is its usefulness. I know that we can allocate dynamic memory or static memory for any variable.

author: Siwastaja
author_rank: Super Contributor
timestamp: February 12, 2022, 12:33:30 pm
content: Example: Your program is a photograph image editor.User opens a 100Mbytes image file. You allocate 100MB for the image. Once the user closes the image, the memory is freed.Now if user opens a 100MB, 10MB and 1MB images at the same time, 111MB of memory is actually consumed. Rest is free for opening more images, or running different programs on the OS.Other option? Statically allocate for 10 100MB images. This requires whopping 1000MB before anything happens, and user can't open any image bigger than 100MB, and is limited to max 10 images at the same time.This idea is language independent. Some languages do this more automatically for you. In C, you use malloc() to allocate, calloc() to simultaneously allocate and clear to zero, realloc() to change the size of allocation (for example, if image becomes bigger or smaller during editing), and free() to deallocate when done.Or, are you thinking more about typical small microcontroller applications? In those, you don't often find real use for dynamic memory allocation, and indeed, if you don't need it, do not use it in small embedded applications because then it might do more harm than good. Use wisely.

author: Nominal Animal
author_rank: Super Contributor
timestamp: February 12, 2022, 05:20:49 pm
content: On embedded microcontroller implementations, dynamic memory allocation is rare. However:Let's say you have a controller device intended to be used in greenhouses. It uses some kind of clever networking scheme, where the sensors can be enumerated at any point in time. Lets say you need to support a few hundred sensors per controller, but the sensors can vary from temperature to moisture to CO2and so on.Do you reserve memory statically for each sensor type (because they need different structures in memory to record their state)?Or do you instead have some memory set aside as a "pool", from which you allocate the needed structures, one per sensor, as they are enumerated?Granted, in C, you probably would not usemalloc()/free()for this (because on many microcontroller base libraries it uses a single pool of memory shared with stack), and instead write your own dedicated and optimized allocator. (For example, that allocator can quite easily "repack" the structures to avoid memory fragmentation, if there is a separate array, pointing to each sensor, for quick lookup, and no other references to each structure except between request and response to that particular sensor.)Similarly, whenever you have a situation where you support a limited number of run-time-detected modules or clients, but those can be of different types and require a different structure in memory to properly handle them, you'll probably wantsome formof dynamic memory management to make sure you can support the maximum number of modules/clients, without requiring extra hardware (like external RAM). Often, it is cheaper to buy that extra hardware per unit, because it is cheaper than the software development cost for the software to run on cheaper hardware but provide the same features.I suspect that IP networking and especially TLS-secured connections (https in particular) is where dynamic memory management starts beingnecessary. In particular, TLS has many differentkey exchangemechanisms and manyciphersboth ends need to agree upon, to form the secure connection. This phase can easily require kilobits of RAM that are not needed afterwards. Some IP networking implementation only support one or a small number of connections, to avoid this; but using TLS it becomes more complicated toavoiddynamic memory allocations than to use dynamic memory allocation, unless you reserve the maximum amount of RAM, and do not mind it being unused for almost all the time. If the IP networking implementation limits to a single connection, it can probably use a common buffer for the handshaking data and TCP data transfer buffers, but that is very easy to cripple (Denial of Service) via repeated connection attempts; a more complicated implementation can be much more robust. The fact that e.g. BSD and Linux IP networking implementations are pretty darned rock solid (even though they are underconstantattacks) shows that it can be done.In all cases the pattern is the same: there are more possibilities for use, than one can statically allocate for. Thus, some kind of sharing mechanism is needed. Dynamic memory management is just the name for this sharing-of-memory.

author: DiTBho
author_rank: Super Contributor
timestamp: February 12, 2022, 05:54:34 pm
content: Code:lib_btree_other # make run#info, instrumented version, r194 (Sat Feb 12 2022)#info, lib_btree_z_v1/btree_init_property: "initialized"#info, lib_btree_z_v1/btree_init_methods: "initialized"#info, lib_btree_z_v1/btree_init_bkpool: "initialized"bkpool has 4 total blocksbkpool has 3 empty blocks> disk_formatdisk "disk.bin", blocks low level initialization|.................................||.................................||.................................||............................. |> coin_add 0..21QA: root has changed: prev=(reserved) curr=00000002 btree_start_newQA: root has changed: prev=00000002 curr=00000007 node_insert_new_rootQA: root has changed: prev=00000007 curr=00000018 node_insert_new_root> coin_list0 1 | 10 11 | 12 13 | 14 15 | 16 17 | 18 19 | 2 20 | 21 3 | 4 5 | 6 7 | 8 9> bkpool_showiblock1=00000007 ticket=0001 _ M 0130 node_insert_new_root.bkpool_block_replace("p_bkroot")iblock1=00000002 ticket=0002 _ _ 0059 node_make.op("p_bknode")iblock1=00000016 ticket=0003 _ M 0002 btree_coins_show.bkpool_block_replace("p_bknode")bkpool'strig: { ticket=3 lowest=2 }blocks:{3 total0 empty712 moved}> disk_blocksdisk "disk.bin":|.d.dddLGddLddLddLGRddLddLddLGddLd||dLGddL...........................||.................................||............................. |> disk_syncbkpool_flushing ... done> disk_blocksdisk "disk.bin":|BdLdddLGddLddLddLGRddLddLddLGddLd||dLGddL...........................||.................................||............................. |>This is an example of "custom" dynamic allocator used for an embedded firmware of an industrial embroidery machine.It's a polymorphic-disk-cached-based b*tree (who? what?)The algorithm basically works on { root_node(R), leaf_node(L), glue_node(G), bmap_node(B), data_node(d) }, which are of the same physical disk-block size: 512 byte. It was initially developed on a Linux-workstation and implemented with malloc() and free() and its keys were not polymorphic ( means able to handle different kind of data-type, e.g. strings rather than integers) but only able to handle integer numbers.Then it got under code-refactoring, and I added the polymorphic layer, but it still it used malloc() and free().Then it got under a new code-refactoring, and I replaced the malloc() and free() functions with a static-allocator of 64MB of size, practically one and half the total amount of ram of the physical target embedded board. It was a kind of dumb-pool written like an array.Then it got under a new code-refactoring again, and I replaced the stalloc code with a block-bkpool model that replaces "pointers" with "iblock" addresses, coupled with a smart allocator that somehow works like virtual memory on PCs.Long story of step-by-step progress but also full of epic failures, catastrophic bugs, and moments so sad that I wanted to give up everything and flee to the north pole. Seriously, "malloc()" is easy to use, while this stuff is very complex and hard to test, and it took me several months to have something working in a decent way.The smart pool allocator is still under testing, in the example above, you can see the b*tree algorithm only has four blocks of memory to store its nodes, one block is reserved (for bitmap, bitmap is used to manage free blocks on the disk), while the allocator continuously flushes and reload blocks during its functioning.The number of blocks is limited, as you can see, in order to insert 20 keys-data nodes it needed to move 712 blocks on a pool of 3 blocks: of course, this is a testing-scenario that I purposely created in this way to *stress* the algorithms. The final product will have more 192 blocks in the bkpool.

author: rstofer
author_rank: Super Contributor
timestamp: February 12, 2022, 11:51:26 pm
content: Any time you are using something like a linked list, you will dynamically allocate nodes added to the list. This is the classic example because not only does it deal with allocation, there is plenty of opportunity to try to use a null pointer. You would also deallocate items removed from the list.There are other ways to do these things but dynamic allocation is the most common.

author: Nominal Animal
author_rank: Super Contributor
timestamp: February 13, 2022, 01:17:56 am
content: Quote from: rstofer on February 12, 2022, 11:51:26 pmWell, not necessarily: consider the array form. The classic example is an array of nodes, where each node has the relative index to the following node (and optionally preceding node), or zero at the final node in the list; plus of course the data for the current node. All nodes are statically allocated as an array beforehand. Unused nodes form one linked list. To add a node to some other list, you pick one from the unused list, fill the data members, and prepend/append/insert it into the list you want. Each list is identified by the index to its first member.Evenbinary heapsare often managed in an array form. (I personally use a binary min-heap with event timestamps to manage lots of timed events using only one timer. If there is a fixed upper limit to the number of timers I need to support, I use a statically allocated array for the min-heap.) There are no indexes used, because the node at indexk(starting at 0) has its parent at(k-1)/2(integer division), and two child nodes at2*k+1and2*k+2, unless they exceed the valid indexes in the array.A related data structure, thedisjoint-set, is best implemented in array form, but logically forms a set of trees (until flattened to a direct lookup array for fastest use). It is used to determine if nodes belong to the same set or not, by starting from every node separate, then merging pairwise nodes (and all nodes connected to said nodes directly or indirectly) into a single set.Now, one can argue whether the above is dynamicmemorymanagement or not. In C, I consider it dynamic object/structure/element management, since the size and type of the structure used is fixed at compile time.Others disagree, and consider the above a variant ofmemory pools, and thus dynamic memory management.The key difference, in my opinion, is when the type and size of each allocated object/structure is not known beforehand and may vary. Then, you cannot statically allocate separate arrays for each possible size up to the maximum number of nodes; there just isn't enough RAM available for that. So, you set up one or more regions of memory that can be near-arbitrarily split, and used as needed, to hold any structure or data the caller wants. Which definitely is dynamic memory management.

author: nctnico
author_rank: Super Contributor
timestamp: February 13, 2022, 01:39:05 am
content: Quote from: Nominal Animal on February 12, 2022, 05:20:49 pmI ran into this problem recently for a microcontroller and gave it some thought. I came to the conclusion that you will have to make fixed memory allocations (memory pools if you want) that support the largest amount of memory needed by every component of the application at any time in order to make the application work reliable. Ofcourse you could be clever and could lock out tasks from a memory pool for a longer period of time but this gets tricky quickly.On PCs you can run into the same problem but since there is a user at the helm who can restart it and have the ability to use a harddrive as extra memory, running out of memory is less of a reliability issue.

author: DiTBho
author_rank: Super Contributor
timestamp: February 13, 2022, 02:56:40 am
content: Quote from: nctnico on February 13, 2022, 01:39:05 amMy approach here is lazy about HTTPS:- only 4 connections are pre-allocated via a stalloc pool- if the pool is full, the fifth and following clients will be pending in a FIFO- they will only got receive a HTTP (Firefox says "unsafe", lol) page telling to waitThis stuff is applied on a Fon2100 running Xinu. It only has 8MB of ram. The OS takes 3MB.

author: Nominal Animal
author_rank: Super Contributor
timestamp: February 13, 2022, 05:07:52 pm
content: OpenWRTis a good example of a full non-RT operating system (Linux) intended to be used in network appliances, especially routers. It is a full 'hosted' environment despite being embedded, running onlots of deviceswithlimited amount of RAM. However, as typical amounts of memory increase, so does the software requirements... Most of OpenWRT, including the Linux kernel, is written in C.The Linux kernel is one place where dynamic memory management is useda lot, but without any leaks. (Many Linux machines keep running without interruption for years on end;anykernel memory leaks during normal operation would make that impossible, and require rebooting at more or less regular intervals.)However, the Linux kernel code is definitely advanced stuff. With a minimal userspace and a trimmed kernel (unnecessary options removed), something like 8 MiB of RAM is sufficient for a complete, full-featured Ethernet and WiFi stack. (It's the rest of the stuff, the user interfaces and the userspace services, that cause the recommended amount of RAM available to be so huge (128 MiB) nowadays.)Considering how microcontrollers like NXP i.MX106x series (as in Teensy 4.x) already have 1 MiB of RAM available, and ESP32-S2-WROVERs have 2 MiB, even on microcontrollers we are approaching the point where dynamic memory management – that is, using a "generic" pool of memory, which is portioned and provided for each task at hand as needed, and then returned back to the pool; for example via Cmalloc()/calloc()/realloc()/free()– becomes useful enough to use, even considering the much increased complexity of the code.In every case, us developers need to make decisions. The complexity of the code is an important one. Often it is cheaper and more reasonable to simplify things, even if it means more expensive hardware is needed. I like to do the complex approach myself on my hobby projects, because I find it fun and interesting and challenging; but it would not make sense if I did this as a job; I'd spend way too much time and effort on things that do not yield corresponding rewards. (Poor Ed Kloonk has been waiting for almost two weeks now for me to do a small change to a program –– volume peak meter –– I posted here a year ago, because I just cannot add a kludge on top of a hack, and instead have the innateneedto clean it up and make it more reliable and useful.)The underlying question,"When to use dynamic memory allocation in C"–– is a hard one to answer. It always depends on the situation and developers involved.When writing C in a fully featured operating system, there is oneanti-patternI often encounter and rant about that I'd like to point out:avoidingdynamic memory management when it really is useful.A particular case is when usingsnprintf()(or worse,sprintf()) to construct a formatted string, using a statically allocated buffer that is arbitrarily limited to some size, hopefully large enough.The following implementssprint2()for use with any C standard library (version C99 or later), and more efficientsprint()for use in Linux/MacOS/Android/BSDs:Code:#define _GNU_SOURCE#define _DEFAULT_SOURCE#define _BSD_SOURCE#define _XOPEN_SOURCE 500#include <stdlib.h>#include <stdarg.h>#include <stdio.h>/* printf() to a dynamically allocated buffer.Given a printf() format (and optionally data to be formatted),this function returns a dynamically allocated pointer to theresulting string.*/char *sprint(const char *format, ...){va_list args;char *ptr = NULL;int len;if (!format) {fprintf(stderr, "sprint(): NULL format string specified!\n");exit(EXIT_FAILURE);}va_start(args, format);len = vasprintf(&ptr, format, args);va_end(args);if (len < 0) {fprintf(stderr, "sprint(): vasprintf() failed, most likely due to running out of memory.\n");exit(EXIT_FAILURE);}return ptr;}/* Alternate, slower implementation for any C99 or later standard library.*/#ifndef SPRINT_MIN_SIZE#define SPRINT_MIN_SIZE 64#endifchar *sprint2(const char *format, ...){va_list args;char *ptr;size_t size = SPRINT_MIN_SIZE;int len;if (!format) {fprintf(stderr, "sprint2(): NULL format string specified!\n");exit(EXIT_FAILURE);}ptr = malloc(size);if (!ptr) {fprintf(stderr, "sprint2(): Out of memory.\n");exit(EXIT_FAILURE);}while (1) {va_start(args, format);len = vsnprintf(ptr, size, format, args);va_end(args);if (len < 0) {fprintf(stderr, "sprint(): vasprintf() failed, most likely due to running out of memory.\n");exit(EXIT_FAILURE);} elseif ((size_t)len < size) {return ptr;}free(ptr);size = (size_t)len + 1; /* +1 for the end-of-string NUL char */ptr = malloc(size);if (!ptr) {fprintf(stderr, "sprint2(): Out of memory.\n");exit(EXIT_FAILURE);}}}int main(void){const char *s = "Hello";int x = 4;double d = 5;char *example = sprint2("s=\"%s\", x=%d, and d=%g.\n", s, x, d);fputs(example, stdout);free(example);return EXIT_SUCCESS;}You use thesprint()/sprint2()function exactly like you would useprintf(), except that instead of printing to standard output, it returns a pointer to the dynamically allocated string. You do whatever you want with it, and thenfree()it when you're done.(We could wrap the function definition in preprocessor macros that choose the appropriate one for the currentoperating system and C library, so that one could always use justsprint(). I was just too lazy to do it here.)I have similar rants about reading lines from standard input or files or other streams usingfgets(), when all but MSVC providegetline()andgetdelim(), that can allocate and reallocate the input buffer as needed. For example, to output one or more files (or standard input) with line numbering, with both CR LF and LF newlines supported even in the same file:Code:#define _POSIX_C_SOURCE 200809L#include <stdlib.h>#include <string.h>#include <stdio.h>#include <errno.h>int do_file(FILE *in, unsigned long *linenumptr, FILE *out){unsigned long linenum = 0;char *line = NULL;size_t size = 0;ssize_t len;int err = 0;if (!in) {fprintf(stderr, "do_file(): NULL input stream.\n");errno = EINVAL;return -1;}if (!out) {fprintf(stderr, "do_file(): NULL output stream.\n");errno = EINVAL;return -1;}if (linenumptr)linenum = *linenumptr;while (1) {len = getline(&line, &size, in);if (len < 0)break;linenum++;/* Remove newline (LF, or CR LF) at end. */if (len > 0 && line[len - 1] == '\n') {--len;if (len > 0 && line[len - 1] == '\r')--len;line = '\0';}/* Print the line number, colon, and a space. Check for errors. */if (fprintf(out, "%lu: ", linenum) < 0) {err = EIO;break;}/* Since the input may contain embedded NUL chars, we use fwrite() instead of fputs(). */if (fwrite(line, 1, (size_t)len, out) != (size_t)len) {err = EIO;break;}/* Append a standard newline. */fputc('\n', out);}/* The dynamically allocated line buffer is no longer needed. */free(line);line = NULL;size = 0;if (linenumptr)*linenumptr = linenum;/* Read error? */if (!err && (ferror(in) || !feof(in)))err = EIO;/* Write error? */if (ferror(out))err = EIO;/* Return 0 if no errors. */if (!err)return 0;errno = err;return -1;}int main(int argc, char *argv[]){unsigned long lines = 0;const char *name;FILE *in;if (argc < 2 || !strcmp(argv, "-h") || !strcmp(argv, "--help")) {const char *arg0 = (argc > 0 && argv && argv && argv != '\0') ? argv : "(this)";fprintf(stderr, "\n");fprintf(stderr, "Usage: %s [ -h | --help ]\n", arg0);fprintf(stderr, " %s FILENAME | - ...\n", arg0);fprintf(stderr, "\n");fprintf(stderr, "This program will output the named files with continuous line numbering.\n");fprintf(stderr, "For standard input, use '-' as the file name.\n");fprintf(stderr, "\n");return EXIT_SUCCESS;}for (int arg = 1; arg < argc; arg++) {if (!strcmp(argv, "-")) {name = "(standard input)";in = stdin;} else {name = argv;in = fopen(name, "rb");if (!in) {fprintf(stderr, "%s: %s.\n", name, strerror(errno));return EXIT_FAILURE;}}if (do_file(in, &lines, stdout)) {fprintf(stderr, "%s: %s.\n", name, strerror(errno));return EXIT_FAILURE;}if (in != stdin) {if (fclose(in)) {fprintf(stderr, "%s: Read error (while closing file).\n", name);return EXIT_FAILURE;}}}return EXIT_SUCCESS;}Most of the code is error checking.getline()reads a new line from the stream, and also takes a pointer to a dynamically allocated buffer and a pointer to the size allocated to the buffer, with NULL and 0 indicating "no buffer allocated yet", and (re)allocates a buffer large enough for the next line.This is rock solid. Even lines containing embedded NUL bytes are output correctly, because it usesfwrite();fputs()would truncate the line at the first embedded NUL byte. A simple cleanup function could be used to remove embedded NUL bytes.Error checking is not something that can be left to be learned later on, or "to be added if I have time later on". It is either baked in, or it is an afterthough bodge; like fixing a broken part with bubblegum and spittle.Dynamic memory management in C is not really complicated. When running a full operating system, all dynamically allocated memory is freed by the OS kernel anyway, so usually no memory allocations are freed just before the program exits. Otherwise, whatever you allocate, youfree()when no longer used. You just must make sure you don't accidentally leave a pointer pointing to the freed memory and try to access the freed memory that way; this is a common bug, "use after free", and can be hard to debug unless you "poison" the data before freeing. (Poisoning is a fancy name for clearing the data to an invalid value.)

author: nctnico
author_rank: Super Contributor
timestamp: February 13, 2022, 06:05:43 pm
content: Still having to allocate and free is very prone to error. A better approach is to allocate data on the stack (if you need relatively small chunks of memory). In C you are allowed to size arrays at runtime and those go onto the stack. This also very elegantly allows to have a variable buffer size vsnprintf() .If you need to use malloc/free a good approach is to switch to C++, use objects and make sure all allocated memory is freed when the object is destroyed.

author: Nominal Animal
author_rank: Super Contributor
timestamp: February 13, 2022, 08:32:29 pm
content: Quote from: nctnico on February 13, 2022, 06:05:43 pmIt is more complex than static allocations or local stack allocations (alloca()in C), but I would not say it is inherently prone to error.Because it is more complex, and many programmers do not bother to consider or track the lifetime of the dynamically allocated objects, it is common to have bugs in this. But it is more like people who do not bother to differentiate betweenwhichandwitch, or writewould oforcould ofinstead of the correctwould haveorcould have. Some do not see the difference – or the need to consider the lifetime of the dynamically allocated objects – worth considering, andthatis why the errors occur.In my examples above, both the 'buf = sprintf(...); use(buf); free(buf);' and the 'len = getline(&line, &size, stream); use(line, len); free(line); line = NULL; size = 0;' patterns are absolutely rock solid. Yes, you do need to check for errors, but no more than when using a statically allocated buffer. Less, if you (like me) consider it okay for a program to abort with an out-of-memory error message, instead of trying to work around it, like in the above examples.Furthermore, there are even more complex things that a C developer needs to worry about, when they use C 'in anger'. A good example is nonblocking I/O, usingselect()/poll()/epoll(). One of the rants I often end up in, is that then using Unix/POSIX low-level I/O,read()andwrite()from<unistd.h>, the OS kernel is allowed to return a short count, i.e. read or write only some of the initial data in the buffer; and that there is no guarantees that the full buffer will be read or written. I often get the retort that "it never happens with normal files", but that is not true: POSIX signal delivery to an userspace handler that was installed withoutSA_RESTARTcan interrupt even file reads/writes. Plus, it can happen with pipes and FIFOs and most sockets (except that with just created pipes and socket pairs have certain initial atomicity guarantees that depend on the operating system) at any point. It often happens with network sockets, but many programmers think it is because network sockets are somehow special: they are not. It is just that ordinary local filesalmost alwaysbehave nicely, and because us humans are stupid, we have started believing that just because they almost always behave nice,we can expect it to always behave nice. Which isn't true.I have also shown my ownpreferred approach to linear algebra/matrix interfaces in C. It uses a simple matrix structure to represent any matrix or matrix view, and refers to reference-counted instance of a data owner object. The numerical data is stored in the data owner, and the data owner counts how many matrices refer to it. When the last matrix using it is destroyed, also the data owner is destroyed. Because matrix views do not differ in any way from "normal" matrices, you can have two matrices, saytandt_transposed, refer to the exact same data, but with the latter having the data already transposed. Any change in one, is immediately visible in the other. You can have views as individual rows or columns into another matrix, any diagonal, or any sub-matrix you want. It Just Works. And the code itself is trivial, immediately obvious from the data structures.As long as you remember to destroy every matrix you create or obtain in any way, you don't need to worry about memory allocation at all.Thus, management of dynamically allocated objects can be made very easy and robust as well.Granted, if one were to use e.g. Boehm GC –– a garbage collection library –– then the programmers would not even need to remember to destroy the matrices they don't need anymore, but I suspect that is the sort of hand-holding attempt that turned PHP from a promising idea to a pile of crappy security hole opportunities. When one uses C, they need to understand that the tool they wield is powerful, and has no safety guards that keeps ones fingers and data intact if you make an awkward move. If you aren't careful enough, C will cut off your arms and legs; it will happily let you createnasal daemonsall day long.However, its low-level power is the other side of the danger coin. The fact that it does not try to stop the developer from doing something stupid/risky, is largely what makes it so powerful in the first place.

author: nctnico
author_rank: Super Contributor
timestamp: February 13, 2022, 11:51:28 pm
content: Recently I spend the better part of a week driving around with valgrind to fix a whole bunch of leaks in a C program that I inherited and which was direly needed for a demo. So forgive me for not being a fan of malloc & free at the moment

author: Nominal Animal
author_rank: Super Contributor
timestamp: February 14, 2022, 12:38:55 am
content: Quote from: nctnico on February 13, 2022, 11:51:28 pmI don't do Perl because of having had to work with a horrible code base (even though I like the authors). So I know how you feel. However, it's not the language's fault that crappy code gets written in it; we humans can effortlessly crapifyanything and everything.

author: westfw
author_rank: Super Contributor
timestamp: February 14, 2022, 07:27:19 am
content: QuotePretty much ALL of the data that I want to dynamically allocate is MUCH too long-lived (and much too "shared") to put on the stack(s)Also, in many embedded systems, the stack is relatively limited in size, while the heap can consume all of free memory.(I suppose you can say that any form of multitasking is also error-prone, dangerous, and to be avoided, but that certainly seems to be widely violated.)

author: Siwastaja
author_rank: Super Contributor
timestamp: February 14, 2022, 07:34:03 am
content: I have also written horrible C beyond salvation in a stressful situation with lack of proper resources. I made it super clear to everyone it needs total replacement. After I left, I felt sad to see an announcement that the company is looking for a C guru to salvage the 30000LoC project. This changed to joy after hearing no one took the job and the main perpetrator ended up in pretty long jailtime for unrelated businesses (ran with similar attitudes, though).

author: DiTBho
author_rank: Super Contributor
timestamp: February 14, 2022, 11:35:55 am
content: Quote from: Nominal Animal on February 13, 2022, 08:32:29 pmLOL, this is too hilarious expression, which not only it's absolutely true in C programming, but it's also a distill of *THE* problemQuote from: Nominal Animal on February 13, 2022, 08:32:29 pmYup. In my case, I "(re?)invented" the wheel(1) with "smart-pointers": an object-like-made struct with function pointers used as callback instead of a simple classic pointer.This way when I pass smart-pointers along functions, each function can in turn directly interact with the pool in a coherent way, and the pool can do the same, so when you need to read or modify something pointed by a smart-pointer, if the pointed block is not in memory it will be loaded, while if it's already in memory it will be also flushed back to the disk.This also implies intelligence in the pool allocator, in order to avoid to waste I/O cycles. That's why I also used callbacks.In my B*tree implementation you have to deal with both disk blocks, memory, and an algorithm that believes it can address infinite blocks (from 0x0000.0001 to 0xffff.fffe, ~ (4G - 2) blocks ) while the pool can only store a few blocks in memory (currently only 4 blocks), so in each instruction the pointers could not be valid and you can't assume anything.Probably it's easier in C++, but I don't know about. I know, in C ... I had to worry about memory<->disk coherency, and minimal I/O cycles.(1) basically, it's what a "classic cache" system does, just embedded and hidden behind a C-typedef.

author: Miyuki
author_rank: Frequent Contributor
timestamp: February 14, 2022, 04:14:43 pm
content: The main thing is you cannot have "real" dynamic memory on MCU or any processor without MMUSooner or later it will ever end up with fragmentation with no way to solve itBlocks must have fixed locations or be every time created and released in a fixed order

author: Nominal Animal
author_rank: Super Contributor
timestamp: February 14, 2022, 06:05:23 pm
content: Quote from: Miyuki on February 14, 2022, 04:14:43 pmAbsolutely incorrect. Even (older versions of) the Linux kernel can run MMU-less. A memory management unit can provide virtual memory and/or limit access to regions of memory, but is in no way necessary for dynamic memory management. In particular, seeuClibc-ng, a standard C library implementation for MMU-less processors.Furthermore, indirect pointers can be used to avoid memory fragmentation. Here is an example implementation in C.First, we have an indirect pointer structure:Code:struct iptr {uint32_t offset;uint32_t length;int16_t used;int16_t refs;}We could use a direct pointer instead of offset, but if we use an offset to the dynamic memory region, this can be used in fully featured C code running under and actual OS (where the region can shrink and grow, requesting more memory from the OS, and returning unneeded memory back to the OS), as well as on a microcontroller.Thelengthfield is optional, and is useful if the indirect pointer is accessed via a helper function (sayget_u32(id,offset,errorval)) to eliminate buffer under- and overruns; theerrorvalbeing returned if access is attempted outside the buffer (so that no "errors" can ever occur). Theididentifies whichstruct iptris being used, but more on that further below.Therefscounter is a simple reference counter, and starts at 1 when the memory is allocated. However, if these objects are used in a list or a tree or in other ways refer to each other, theusedcounter can be used to make sure that an allocation still in use is not freed before it is no longer used. A typical interface isget_iptr(id)/iptr_ref(id)to increment the counter, andput_iptr(id)/iptr_unref(id)to decrement the counter. There is no "free()" per se; whenever therefscounter drops to zero, the region will be freed.Theusedcounter is a simple lock, indicating the data itself is being used. A typical interface isiptr_use_begins(id)at the beginning of some operation that uses the dynamically allocated region, with a correspondingiptr_use_ends(id)when that access or loop completes.The 'trick' is that when theusedcounter is zero, the data can be moved in memory, compacting the dynamically allocated memory area, so that the memory used by these allocations can be periodically defragmented. If all currently usedstruct iptr usedcounters are zero, then the compaction is a very efficient single copy loop over the data.A practical C implementation (that manages the heap size dynamically by obtaining memory from the OS) might haveCode:volatile unsigned char *heap_ptr = NULL; /* Beginning of dynamic allocation area */volatile size_t heap_len = 0; /* Size of dynamic allocation area */int indirect_num = 0;struct iptr *indirect_ptr = NULL;/* Compact dynamic allocations, defragmenting dynamic memory regions.Returns the size of the largest free fragment. */size_t imem_compact(void);/* Allocate size * count bytes, returning the ID of the memory.Returns negative if not enough memory available.This function will call imem_compact() if necessary. */int imem_alloc(size_t size, size_t count);/* Mark the allocated memory as used by something else.Before the memory gets actually freed, as many additional imem_discard()calls need to be made as the calls to this function. */void imem_obtain(int id);/* Discard previously allocated memory. */void imem_discard(int id);/* Begin an access to the allocated memory.Returns a direct pointer to the data, or 'impossible' if invalid.NOTE: Each begin must be paired with a corresponding end,and the interval between the two should be minimized. */*/static inline void *imem_use_begins(int id, void *impossible);/* End of accesses via the pointer obtained via imem_use_begins(). */static inline void imem_use_ends(int id);where the last two are rather simple,Code:static inline void *imem_use_begins(int id, void *impossible);{/* Check that the ID number refers to a valid iptr. */if (id < 0 || id >= indirect_num)return impossible;/* Check that the iptr can be referenced. */if (indirect.refs <= 0)return impossible;indirect.used++;return heap_ptr + indirect.offset;}static inline void imem_use_ends(int id){if (id >= 0 && id < indirect_num && indirect.refs > 0 && indirect.used > 0)indirect.used--;}Additionally, you probably would implement helper functions depending on the actual data dynamically allocated, for example to copy and compare, or get and set individual values:Code:static inline bool imem_copy_from_ptr(int dst_id, int dst_offset, int length, void *src);static inline bool imem_copy_to_ptr(void *dst, int length, int src_id, int src_offset);static inline bool imem_copy(int dst_id, int dst_offset, int length, int src_id, int src_offset);static inline int imem_cmp_ptr(int id, int offset, int length, void *ptr, int invalid);static inline int imem_cmp_imem(int id1, int offset1, int length, int id2, int offset2);static inline uint32_t imem_get32(int id, int offset, uint32_t invalid);static inline uint16_t imem_get16(int id, int offset, uint16_t invalid);static inline float imem_getfloat(int id, int offset, float invalid);static inline bool imem_set32(int id, int offset, uint32_t value);static inline bool imem_set16(int id, int offset, uint32_t value);static inline bool imem_setfloat(int id, int offset, float value);On microcontrollers with external RAM that is not fully directly accessible to the processor –– very much like what DiTBho described, with on-disk structures! –– a similar interface can be used to access that external RAM via memory apertures or caches of some sort.These are certainly more complex, and incur run-time access cost (because we cannot justusethe allocated memory, but have to do bookkeeping around each access or set of accesses) compared to, using a statically allocated memory. However, this is definitely doable, and assuming the "user code" does not do any silly errors (like keep an area "in use" just because the programmer thinks they can shave off a few cycles of run time that way, or forgets to end use somewhere), is quite robust, and does not easily suffer any issues related to memory fragmentation.Methods like this were heavily used on 8-bit and even 16-bit processors. At the hardware level,memory bank switchingwas often used. Even today, there are microcontrollers using bank switching; there, an unique pointer is actually a tuple, (bank,address), because the contents of the banked address range depends on the active bank. Note that this is not really "virtual memory", because we cannot control the addresses; only the data visible in the banked address range can beselected(from a small number of options, depending how many banks are mapped to the same region by the hardware). In many cases, the bank selection was actually just a standard serial-to-parallel latch, that controlled the highest address lines to the memory chips, as the processor or microcontroller itself did not have sufficient address lines to access all available memory. Sometimes, the bank switch only switched between ROM and RAM, by manipulating the chip enable pins on the ROM and RAM chips connected to the same address and data buses.Personally, I have no problems in using dynamic memory management even on small microcontrollers like AVRs. (The Atmel ATmega32u4 is my favourite 8-bit microcontroller.) In my use case (usingavr-gcccompiler andavr-libcC library), the heap (region of memory dynamic allocations happen from) and stack are a single shared region, with allocations preferring the low end, and stack growing downwards from the high address end. I have no problems with memory leaks; and the types of things I use memory allocation for, memory fragmentation is not an issue either.What I do have trouble with, is determining whether at runtime the stack and heap collide/overlap. There are many methods to detect if such a collision has occurred (heuristically; no false positives, but with a small chance that a collision might be missed), for example viacanary values, but at that point the operation is already unsafe and the device should be restarted... testing basically consists of using canary values or similar techniques, perhaps periodically measuring the difference between the top of the heap and bottom of the stack (to determine the amount of leeway between the two),and then trying to run the device through all possible scenarios. There are free and commercial simulators that can do static (just analyzing the code) and dynamic (running or simulating running the code) to stress test and find these things out, but they are architecture-specific, and the best ones mostly cost a lot of money.Only in the simplest cases can I prove how much heap will be used, how fragmented it can be; how much stack will be needed in the worst case; and if the available RAM for both is larger than that, can I prove that the two will not collide.In other words, this stuff is quite complicated andsensitive, the moreso the less RAM you have available, but not technically impossible.When others (and I guess myself too!) say that dynamic memory management is not often used with microcontrollers, they do not really mean it is technically impossible; they meanit is not worth the effort. That if you go down that road without enough experience and skill, you will find very little success and a lot of frustration.Yet, it is a skill that can be taught, much more so that say playing go or chess. I find it frustrating that so few C programmers are interested in such skills, and instead concentrate onbeing fast at writing code, because that's what employers pay for. (The frustration stems from the fact that the results are becoming less and less reliable, with things likeCVE-2021-4043being commonplace. It's a severe local privilege escalation (letting any local user on the machine get full superuser privileges), due to a very silly programming bug, that nobody caught in over a decade.)Thus, advise that says it is not a good idea to use dynamic memory management on a microcontroller with very limited amount of RAM, stems from practical reasons and human stuff, and not technical details. You could say very concisely that"most often, it is not cost-effective to use dynamic memory management on a microcontroller in C". It does not mean it cannot be done, just that the effort needed to make it work is usually too much, compared to the rewards.I have a bit of a dichotomy here.On one hand, I want to encourage programmers to use dynamic memory management, and get good at it, because when you do get good at it, you can do even those complex things even in constrained environments, and achieve Very Nice Results. On the other hand, if you want programming to be your career, beingreasonableis much more valuable. Things like continued maintenance, documentation, and even writing useful comments (describing developer intent and reasoning, and not what the code does)), algorithm analysis, algorithm implementation, general problem solving skills, and software design (modularity, software engineering, and so on); are much more valuable (in terms of employment and wages) than skills in a detail like dynamic memory management.So, when someone like Dadu@ asks about this, I want to both showhowandwhy, but also show why you might NOT want to do it.The end result? Walls of text with interspersed code snippets like this post, that nobody has the time or will to read. Apologies.

author: emece67
author_rank: Frequent Contributor
timestamp: February 14, 2022, 06:25:39 pm
content: .

author: DiTBho
author_rank: Super Contributor
timestamp: February 14, 2022, 07:58:43 pm
content: Quote from: Nominal Animal on February 14, 2022, 06:05:23 pmyup, for example theLineo uCsimm/uc68ez328board (2001-2003) doesn't have any MMU under the hood but runs the kernel Linux 2.2 patched with uclib-linux-ng (flat binary) stuff.Arcturus Networks had something similar.

author: DiTBho
author_rank: Super Contributor
timestamp: February 14, 2022, 08:11:13 pm
content: Quote from: Nominal Animal on February 14, 2022, 06:05:23 pmNot a coincidenceVladimir said he has a stack of kit to dispose of this year to downsize a lifetime's collection and to free up space, so he sent me aLineocard (in exchange for a a couple of good bottles of vodka), an he was so kind to also include a CD with the original support provided by Lineo back in 2001.That code inspired my solution.

author: NorthGuy
author_rank: Super Contributor
timestamp: February 14, 2022, 09:44:11 pm
content: I don't think the problem with heap allocation is that you need to free what you allocate. Certainly, if you don't allocate something, or if you free too early, or if you free twice, the effect may be devastating and unpredictable. But this is rather easy to detect and fix.One problem is fragmentation. The heap may fragment, in which case the memory may become unusable for further allocations. With arbitrary size of the allocations, and arbitrary lifespan, it may be difficult to predict how bad the fragmentation may be, and therefore difficult to asses how much memory you need.Another big problem is that an allocation may fail. In many cases, it is very difficult to figure out what to do if allocation fails. If you want to do something sensible for every failed allocation, you may need to write lots of code with this sole purpose, and it will take long time to test all the possible allocation failures.On PCs, where you never will face fragmentation (as the memory may be remapped by the OS), and your allocations will never fail unless you have a memory leak (as the memory is abundant), using heap is easy and an average program will use lots of dynamic allocations.On a small MCU though, these two problems are very real. It may be worth avoiding heap allocations alltogether. There are other forms of dynamic allocations, such as stacks(LIFO) or queues(FIFO) which you can use to allocate objects of varying sizes (although C doesn't have any buit-in support for that aside of the call stack). If your allocation pattern allows using a stack or a queue instead of a heap, it may be worth considering. These things will never fragment, although you still can run out of memory. Of course, static allocation is the most reliable approach.

author: nctnico
author_rank: Super Contributor
timestamp: February 14, 2022, 10:08:40 pm
content: In addition to what NorthGuy wrote: at some point you will want / have to provide proof that your implementation has a predictable behaviour. When sharing memory with dynamic allocation between several tasks, this is almost impossible to do. With fixed blocks of memory using pooling with fixed element sized or FIFO buffers, the proof is much easier to provide.Take LWIP for example: with LWIP internal memory pooling, you can predict the amount of memory it needs -worst case- per connection precisely. This means that you can set clear boundaries for when the system behaves as expected and at what point it will break.

author: dietert1
author_rank: Super Contributor
timestamp: February 14, 2022, 11:18:00 pm
content: Maybe one term to mention is "handle". I met that first learning the original Macintosh OS on one of the shoebox Macs. They used another layer of indirection to keep most of the memory blocks moveable in order to facilitate heap defragmentation. I think it existed in MS Windows, too, but was later absorbed into hardware. Don't know whether that method still exists in one of the free operating systems for MCUs.Once i managed to load a 2 MB text file into the 4 MB RAM of the MAC after several days of try and error. Don't remember what it was meant to do there.Regards, Dieter

author: dare
author_rank: Contributor
timestamp: February 15, 2022, 12:46:25 am
content: QuoteThis isn't really true. The current discussion is around the use of application-level heap allocators such as malloc/free, not OS page allocators. In this context, desktop and server applications can absolutely experience fragmentation. And when this fragmentation gets bad enough the result can indeed be a failure of the application (which I've seen a number of times in my career). The existence of an MMU and the OS's ability to do things like paging doesn't change this.It is more correct to say that, in general, the effects of fragmentation are easier to ignore in desktop/server applications because most of these applications run on systems where the total amount of memory is significantly larger than the application's peak memory requirements. As others have said, once you get into small embedded environments, where total memory is much more constrained, this relationship doesn't hold and heap fragmentation can become a major issue.

author: peter-h
author_rank: Super Contributor
timestamp: February 15, 2022, 06:38:47 am
content: The heap isn't a problem on PCs because you have loads of RAM, and the machine gets rebooted periodically anyway because "something has crashed / is not working right / a printer has disappeared / windows explorer has hung because a remote connection went dead / etc". I usually manage a few months with win7-64 but no more due to a broad mix of apps.On embedded systems, it is best avoided.I use it in specific cases where an optional feature is activated and needs a chunk of RAM, and that feature never terminates (until the next reboot). Then it is safe.I am not sure LWIP uses malloc(). TLS (MbedTLS) certainly does, and it can either use your heap, or you can give it a static buffer and it runs its own heap inside that. AFAICT itisa potential (non deterministic) problem, but the usage gets thrown away when the TLS session terminates so you do have some sort of control over it. I am not working on that part of the project...This is an example of malloc() for an optional feature in a product:Code://static uint32_t wavebuf0;// for buffer 0//static uint32_t wavebuf1;// for buffer 1// This method does not waste static memory if the RTOS thread is not started.// It assumes that the task will never terminate - will get a memory leak otherwise!uint32_t *wavebuf0 = malloc(SPC*sizeof(uint32_t));uint32_t *wavebuf1 = malloc(SPC*sizeof(uint32_t));if ( (wavebuf0==NULL) || (wavebuf1==NULL) ){#ifdef DEBUG_USBdebug_thread_printf("WAVE malloc failed");#endifif (wavebuf0!=NULL) free(wavebuf0);if (wavebuf1!=NULL) free(wavebuf1);// hang for ever if malloc failedwhile(1){osDelay(1000);}}

author: brucehoult
author_rank: Super Contributor
timestamp: February 15, 2022, 10:52:12 am
content: Quote from: Nominal Animal on February 14, 2022, 06:05:23 pmI look at things another way.There are tasks which inherently require dynamic memory management, based purely on the nature of their interface with the outside world.For example, suppose we have a specification for a device that has a communication channel -- UART, WIFI, I2C ... it doesn't matter -- which sits in an event loop accepting the following messages (I will give it in text, but it could just as well be a binary format):Code:PUT key "value"GET keyDEL key"value" can have any length, at least up to some fairly high maximum .. it doesn't matter in principle whether this is a few hundred bytes or a few GB.key might restricted to something fairly short. Let's say 32 characters.PUT associates the value with the key for later retrieval by GET. DEL forgets the value associated with key.You simply *can't* use static memory allocation for this unless you are prepared to limit the values to relatively small ones AND limit the total number of keys to your storage size divided by the maximum size of a value. Which, assuming that the average value is much smaller than the maximum possible one, is very inefficient.

author: nctnico
author_rank: Super Contributor
timestamp: February 15, 2022, 11:53:26 am
content: Quote from: brucehoult on February 15, 2022, 10:52:12 amThat leads me to these questions:1) What else are you going to use the memory for?2) How are you going to guarantee that there is always enough memory to handle the longest value?3) Do you allow the system to offer limited functionality at times where memory is low?

author: dare
author_rank: Contributor
timestamp: February 15, 2022, 03:38:35 pm
content: Quote from: brucehoult on February 15, 2022, 10:52:12 amI agree you can't use static allocation in this case, but that's based on my fairly pedantic notion of "static allocation"; others may interpret this differently. As I define it, static allocation is when memory is assigned for use at one time (often boot time), never re-used for any other piece of data, and no meta-data is kept anywhere to indicate whether the memory is "in use" or not. Anything other memory allocation scheme is dynamic, at least in some sense of the word.In the case of key-value get/put/del example, one wants dynamic allocation because you want be able to reuse the memory assigned to a key-value pair for a different key-value pair once the first pair has been removed. The question then becomes, what form of dynamic allocation is appropriate? An obvious choice is a traditional heap-based allocator, such as is used in many malloc/free implementations. However with these allocators you run the risk of fragmentation, especially if your total memory is small relative to the peak data set size.The real issue with traditional allocators is not fragmentation per se, but rather the inability to be able to reason about their behavior. Unless you can characterize the allocation pattern *very* precisely, it effectively becomes impossible to know limits of the system. This may or may not be a problem, depending on the requirements of the application.The next obvious choice is a pool-based allocator with multiple pools of fixed-sized memory chunks, preferably tuned to the expected key-value sizes. Such an allocator may, on average, be less memory efficient than a traditional heap-based allocator. But the tradeoff is that it makes it much easier know under what circumstances the system will run out of memory.The "ideal" choice is some form of compacting allocator, where existing key-value pairs are moved in memory to make room for new ones. This could range from some form of general-purpose garbage collection system, all the way to a custom compacting allocator designed specifically for storing these key-value pairs (probably implemented as an integral part of the key-value data structure). The former tends to be big (in terms of code size) and fairly complicated to integrate (and hugely complicated to implement). The latter can be fairly simple to implement, but of course requires custom code for each application. However, the benefit you get from a compacting allocator is near perfect memory efficiency and an easy to understand model for when the system will run out of memory.In the products I've worked on (generally consumer IoT), pool allocators were always the choice for systems with less than 512KB of memory. Above 1MB, heap allocators were often used. But ultimately this is a judgment call based on the demands of the application.

author: Nominal Animal
author_rank: Super Contributor
timestamp: February 15, 2022, 03:40:27 pm
content: Quote from: brucehoult on February 15, 2022, 10:52:12 amCertainly.Quote from: nctnico on February 15, 2022, 11:53:26 amSee mypost #2 in this threadfor an example: greenhouse sensors.Let's assume that the controller supports multiple different sensor types, and the in-memory structure needed for them varies in size and type. Instead of having an array of unions of the different sensor types (so that each slot can support any type of sensor), we can allocate each dynamically, and thus support more of the "simpler" sensors with the same amount of RAM available.Or, for a simpler temperature measuring device without semi-permanent storage (like EEPROM or Flash), we could reserve a single memory area, and share it between the currently available sensors, and their past readings. When you have just one sensor, you have maximum history available too. With the maximum number of sensors, there would be no history available. Or you could even let the user choose how much history they desire per sensor; with the rest just having min/avg/max.Because of heap and stack being the same on AVRs when using avr-gcc and avr-libc, I do usually statically reserve the memory available for such use cases (rather than use malloc()/free()), and just divvy up that memory among the different uses as needed. This, too, definitely is dynamic memory management. (It is a variant ofregion-based memory management; note that it is just not an array of sensors, and that both individual sensor structures and some kind of arrays maintaining their past values, are involved. Not just different types of elements allocated from the same region, but with varying sizes as well.)

author: Siwastaja
author_rank: Super Contributor
timestamp: February 15, 2022, 04:18:09 pm
content: Quote from: Nominal Animal on February 15, 2022, 03:40:27 pmThe problem of this is, while it feels very elegant way of using RAM, the end-user likely doesn't want that configurability, and may misread the specs. When they choose a product, they see a specsheet like this:* Supports up to 256 sensors!* History of sensor values up to 12 months!If they then, after purchase and install, notice that they can only choose a few sensors with 12 months of history, they will be disappointed and claim for money back.OK, so the product itself is fine, the dispute was caused by overeager sales department. But are we going to fix it like this:* Supports up to 256 sensors!1* History of sensor values up to 12 months!11so that number of sensors times history length cannot exceed xxx sensor-months, in case of simple temperature sensors, or yyy sensor-months, in case of more complex sensor type, and between the mix of the two sensor types, interpolate accordinglySo we end up with an elegant, well engineered, configurable system the user does not appreciate. The right choice might have been: just throw more RAM at the problem.Dynamic memory might prove best in those cases where brute-forcing enough memory for all permutations would require totally ridiculous amounts, compared toreal world worst case(which is hard to define rigorously, of course). Then, average case would be even further from the theoretical worst case of all permutations.

author: Nominal Animal
author_rank: Super Contributor
timestamp: February 15, 2022, 04:33:18 pm
content: Quote from: Siwastaja on February 15, 2022, 04:18:09 pmthat humans in general are rather ignorant and prefer to make assumptions instead of actually find out things; yes. Which leads back to the dichotomy I mentioned before (in#17), and to the statement I made that brucehoult quoted (in#26).Quote from: Siwastaja on February 15, 2022, 04:18:09 pmIndeed. That is what I wrote at the end ofmy first post in this thread, too.

author: peter-h
author_rank: Super Contributor
timestamp: February 15, 2022, 05:05:03 pm
content: QuoteI was thinking the same. AFAICT there is no way to do a "heap" which can never fragment, and which cannot be replaced by statically allocated memory (which contains buffers which can be re-used for different things).And heap fragmentation is not acceptable in an embedded system - unless there are mitigating circumstances e.g.- the product can safely reboot when it gets into a mess- the code which uses the heap can be dumped and restarted- you have some garbage collection and some way to guarantee this will never fail (not sure if that is possible, but perhaps by limiting the allocation to small blocks)

author: Nominal Animal
author_rank: Super Contributor
timestamp: February 15, 2022, 06:13:44 pm
content: Quote from: peter-h on February 15, 2022, 05:05:03 pmIndirect pointers, AKA handles, allow defragmentation at run time. I even showed how to do that in C earlier in this thread.[Yes, I am getting annoyed at people claiming that I've already shown how to do, are "impossible". Unrealistic for most use cases, or not worth the effort in typical cases, maybe; but definitely not impossible or even unfeasible or even unprovable (wrt. operational limitations/requirements).]Quote from: peter-h on February 15, 2022, 05:05:03 pmIf those different things are also of different sizes (even if just limited number of different sizes), it is definitely a form of dynamic memory management.In particular,slab allocatorscan be used to make heap fragmentation irrelevant, if the sizes of the allocated objects are similar enough.To repeat, the Linux kernel is used even on memory-constrained MMU-less devices (without virtual memory), and does so successfully, with indefinite uptimes and no requirements for reboots. These use the olderslob allocator. Current Linux kernels use theslub allocator by default. There is a reason why the Linux kernel is used so ubiquitously in appliances. It is almost always the userspace components, and the software integration (making sure those components work as a cohesive system), that makes or breaks a Linux-based system. Hell, most TV's nowadays use a Linux kernel to manage their user interface and connectivity, to say nothing of routers and switches!It annoys me when people confuse "too complex by most developers to implement" with "unreasonable to implement" or "impossible to implement". Some things are quite possible, and as the Linux kernel shows, definitely doable in practice as well; they just require so much experience and skill to implement successfully, that they are not usually reasonable.

author: NorthGuy
author_rank: Super Contributor
timestamp: February 15, 2022, 06:18:16 pm
content: Quote from: Nominal Animal on February 15, 2022, 03:40:27 pmUnless sensors are connected/disconnected dynamically, you don't need a heap for that. At the beginning of your program, you can enumerate all the connected sensors and allocate the necessary memory to each of the sensors from a common pool. As the allocations are never to be deleted, all you need is to obtain the pointer, such asCode:uint8_t pooluint8_t* pool_ptr = pool;...// for each detected sensorsensor_ptr = (sensor_t *)pool_ptr;pool_ptr += sizeof(sensor_t);and nothing else is needed. The only thing you need to worry about is that POOL_SIZE is large enough to support any possible configuration.

author: Nominal Animal
author_rank: Super Contributor
timestamp: February 15, 2022, 06:47:01 pm
content: Quote from: NorthGuy on February 15, 2022, 06:18:16 pmYou didn't even bother to read my post.I explicitly described the type and number of sensors was up to the user at run time; so definitely connected/disconnected dynamically.Yes, yes, "you're too verbose to read your post", but it is FUCKING ANNOYING to try and argue, when YOU don't even bother to read the argument, and just spew out your misunderstandings as if they were facts. If you don't read my posts, just ignore my posts instead of arguing what youbelieveI am saying without actually reading my posts, okay?

author: SiliconWizard
author_rank: Super Contributor
timestamp: February 15, 2022, 07:13:30 pm
content: Not particularly saying this because I started it, but there was a whole thread I started, dealing with dynamic memory allocation and allocators, which I think went over all those questions at some length. Feel free to have a look (or ignore it) - but that's mainly why I didn't post anything here until now, I think I already said a lot of what I had to say about this topic.https://www.eevblog.com/forum/programming/fast-predictable-memory-allocator-for-embedded-stuff/I agree with Nominal here, of course defragmentation of memory using double indirection is doable and was done both on MacOS and Windows a long time ago. Of course this comes at the cost of defragmentation happening at unpredictable moments and taking up unpredictable amounts of time.And of course there are allocation strategies that do not fragment. As we discussed in the other above thread, this usually requires more planning and being more rigorous, while carelessly using "malloc"-like functions ad-hoc, without caring for object lifetime or fragmentation is as lazy as it comes, and is what can lead to issues (unless as some said earlier, there's much more memory available than your application will ever use, in which case malloc() is surprisingly effective - but that's a bit like having *a lot* of cash in the bank, in which case your spending habits won't matter whatsoever... to an extent).

author: nctnico
author_rank: Super Contributor
timestamp: February 15, 2022, 07:17:36 pm
content: Quote from: Nominal Animal on February 15, 2022, 03:40:27 pmSorry but IMHO that post does not really answer these questions; it just outlines the problem I address with the questions above.I agree with the notion that memory that isn't actually used and may never be used is wasted from a technical point of view. OTOH the customer is paying for a device with a defined worst case capacity. That worst case capacity also defines the amount of memory a device needs at minimum.

author: dare
author_rank: Contributor
timestamp: February 15, 2022, 07:55:02 pm
content: When people talk about it being "impossible" to implement a heap allocator that doesn't fragment, I believe they are implicitly talking about a particular type of heap allocator; specifically one that has semantics equivalent to malloc/free (what I call "traditional" heap allocators, dating back to ca. 6th edition Unix). Indeed, for these types of allocators, in which objects cannot move once allocated, it is impossible to implement one that never fragments in any situation (i.e. given any possible allocation/free pattern).As others have noted, if you relax the requirement of fixed addresses then there are other designs that can mitigate fragmentation by means of compaction, using either object handle schemes (double indirection), or pointer fixup to deal with object motion.Technically, the only general allocation scheme that doesn't suffer from any form of fragmentation is copying garbage collection, as by definition, the available free space is always in one contiguous block. However, these have very inefficient memory utilization, which makes them ill-suited for embedded applications.

author: nctnico
author_rank: Super Contributor
timestamp: February 15, 2022, 08:01:32 pm
content: Quote from: dare on February 15, 2022, 07:55:02 pmYes. A long time ago I have implemented a linked list based memory management system for a system that was really memory constrained. This would return a handle and used an API for memory access. The upside is that fragmentation is not a problem and if you need buffers to be contiguous (for temporary reading / writing), you can defragment the memory if necessary. But still, it is not compatible with malloc/free in any way so existing software doesn't work with it.

author: NorthGuy
author_rank: Super Contributor
timestamp: February 15, 2022, 08:09:09 pm
content: Quote from: Nominal Animal on February 15, 2022, 06:47:01 pmI am not arguing with you. I just showed a case where one-time allocation can eliminate the need for heap. This barely has any relation to the things you said.

author: peter-h
author_rank: Super Contributor
timestamp: February 15, 2022, 08:50:32 pm
content: Sure runtime defrag is possible; I didn't say it isn't.

author: brucehoult
author_rank: Super Contributor
timestamp: February 15, 2022, 10:28:33 pm
content: Quote from: nctnico on February 15, 2022, 11:53:26 amAll real world computers are finite. You have to deal with running out of resources no matter what you do.You have 64KB of RAM. You could statically allocate the space and say "You can store up to 64 keys, each with a value up to 1000 bytes long".This might be fine for a customer who actually wants to store 900 byte values -- until one day they want to add a 65th key and it fails.It's definitely not going to be fine for a customer who only has 100 byte values, and would quite like to use the 64KB she paid for to store 640 keys.Not to mention the customer who has a lot of 30 byte values, and a couple of 10 KB ones.I can't see how flexible is ever worse for anyone.

author: brucehoult
author_rank: Super Contributor
timestamp: February 15, 2022, 11:00:45 pm
content: Quote from: dare on February 15, 2022, 03:38:35 pmIt's not like fragmentation is hard to avoid.In the system I used as an example in the previous message, with 64 KB total memory, I would strongly consider dividing all of memory into 16 byte chunks and use each chunk to store up to 14 bytes of either key or value, plus a pointer to the next chunk of the key or value. There will be a free-list of all unused chunks, similarly linked together.This costs 12.5% of potential peak storage space, but provides huge flexibility.The only people who might not think they're getting their money's worth are those with keys or values routinely shorter than 14 bytes. That's another form of fragmentation, but not one that grows with time. I'd consider splitting some of the 16 byte chunks into two 8 byte chunks to be used as the only chunk of very short values (maybe also as the last chunk for longer ones). The two sizes can be distinguished by memory address e.g. the 8 byte chunks are at the highest (or lowest) memory addresses, with the boundary moved as needed.You can build an entire general-purpose memory allocator around these ideas, with multiple sizes in multiple pools etc, but for this specific device an ad-hoc implementation is simple and small in code size.

author: brucehoult
author_rank: Super Contributor
timestamp: February 16, 2022, 12:01:20 am
content: Quote from: SiliconWizard on February 15, 2022, 07:13:30 pmWell before that :-)In 1977, Applesoft BASIC provided string variables, and arrays of strings. Strings were allocated sequentially in memory and when memory was exhausted there was a compacting garbage collection phase. Unfortunately the algorithm used was O(N^2) and could take ... I don't remember precisely ... something on the order of a couple of minutes.One of the very first things I ever wrote as a 17 year old kid was an assembly language (actually machine code as I didn't have an assembler) routine that did the compaction in O(N) time or effectively more like O(1) as the byte copying took more time than the bookkeeping, and each byte was only copied once. It worked by walking the symbol table and replacing the first couple of bytes of each string with a pointer to the symbol table entry (and putting those bytes into the symbol table), then walking the heap sequentially from string to string (using their lengths). Once the first unreferenced string was found the next still alive string was slid down into that space, and so on. Finally, the compacted live strings were walked again to restore their initial data bytes and make the symbol table point to them instead of vice-versa. The whole thing took under a second maximum on a 48k system.The biggest problem was that, as it wasn't possible to replace the built in compaction routine (in ROM), you needed to manually call my routine frequently enough that the built in one was never triggered. (On a machine with a 16k "Language card" I guess you could copy ROM into RAM and patch it, but I didn't try).As AppleSoft was written by Microsoft I guess all their other BASICs had the same bad compaction routine -- which, to be fair, was not usually a problem if you used only simple string variables, not arrays of strings.Woz's earlier "Integer BASIC" didn't need string compaction because it went for internal fragmentation instead: you declared a string likeDIM A$(20)and then could store up to 20 characters in it and anything shorter would waste memory. i.e. effectively a string is just an array of characters instead of an array of integers. I don't think Integer BASIC allowed multi-dimensional arrays (and thus not arrays of strings), but even if it did they would never need compaction.

author: dietert1
author_rank: Super Contributor
timestamp: February 16, 2022, 07:01:50 am
content: In the early Mac OS i mentioned above, double indirection was used to keep most heap blocks relocatable. If i remember correctly, cost was something 10 or 12 Bytes per block including pointers for heap linked lists. The OS supported and required lock and unlock operations at application level to prevent corruption of dereferenced handles during heap defragmentation. So a certain fraction of relocatable blocks would normally be locked and non-relocatable. Difficult to characterize reliability by todays standards.Regards, Dieter

author: brucehoult
author_rank: Super Contributor
timestamp: February 16, 2022, 09:27:22 am
content: Quote from: dietert1 on February 16, 2022, 07:01:50 amIn the early 90s I ran the computers in the research department of a stockbroking company.I had a Mac SE/30 as a server. The machine sat on top of a pile of 3 or 4 SCSI disk drives of increasing capacity, and a SCSI tape drive. It was connected to both LocalTalk and Ethernet. It ran the standard install of System 7, Apple Internet Router to route between LocalTalk and EtherTalk, AppleShare file sharing, and "Fastback" (I think) backup software that was really great because when someone said "My file is corrupted!" I could immediately tell them "There were new versions on X, Y and Z dates with sizes x, y, and z, which do you want?" (often someone would munge up their file and save it and the munged version would get backed up for a few months before they noticed. And then the program would tell me exactly which tape to put in the drive to get that version of the file...Anyway ... this machine several times achieved an uptime of around 18 months. I don't remember it *ever* crashing. A couple of times I had to take it down to add a new larger disk, and I'd do a software update at the same time. Once I had to take it down to move across town to a new office.At the start I think that SE/30 was serving mostly to about a dozen plain SE and maybe still the odd Plus, but as quickly as possible people got upgraded to machines with "Full Page" (640x870) or "Two Page" (1280x870) monochrome displays for their Word and Excel documents, and from LC to LC II to LC III to LC 475 machines.Crashes were really quite rare. Maybe not as rare as on the server SE/30, but rare.

author: dietert1
author_rank: Super Contributor
timestamp: February 16, 2022, 10:40:59 am
content: Yes, at that time Windows used to run as DOS application. There was no way an IBM PC would allocate 2 MB of linear RAM memory. Using lockable handles, heap defragmentation was efficient and ran continuously, as background activity. I think those memory management methods could be useful on F4 and F7 Arm MCUs. Probably there is already some OS or library offer including that.Regards, Dieter

author: Miyuki
author_rank: Frequent Contributor
timestamp: February 18, 2022, 09:05:24 pm
content: Quote from: Nominal Animal on February 15, 2022, 06:13:44 pmI know MMU less Linux is a thing, but it still has the recommendation to avoid using malloc when possible and to use static pools insteadAnd you must have the luxury of time, to stop the execution and physically move data in memory when you end up needing to defragPlus almost all today Linux systems run on CPU with MMU, today when it cost almost nothing. And they want to run things like Android or so on every appliance.

author: Siwastaja
author_rank: Super Contributor
timestamp: February 19, 2022, 07:42:54 am
content: Quote from: Miyuki on February 18, 2022, 09:05:24 pmCould you please just stop coming up with made-up nonsense by putting tech words you don't understand one after each other?Using malloc() in systems without MMU is completely normal. MMU is totally orthogonal to heap allocations. Whether using malloc() is a good idea depends on everything else.

author: Miyuki
author_rank: Frequent Contributor
timestamp: February 19, 2022, 09:24:02 am
content: Quote from: Siwastaja on February 19, 2022, 07:42:54 amI might have old information but all the papers and recommendations on uClinux for Coldfire (it is a pretty typical platform and decades-old)You must be very careful when releasing memory.It is not a flame, I just saw too many devices randomly crashing after a few weeks or months for whatever reason.

author: brucehoult
author_rank: Super Contributor
timestamp: February 19, 2022, 09:36:35 am
content: Quote from: Miyuki on February 19, 2022, 09:24:02 amYou must be very careful and think about your memory use on everything, no matter whether you use malloc/free, a pool allocator, arenas, GC, or whatever. That doesn't mean "don't do it". It means be careful.QuoteMany programmers are not careful.

author: Siwastaja
author_rank: Super Contributor
timestamp: February 19, 2022, 09:59:33 am
content: MMU does not fix memory leaks. Software using malloc() must be careful to use free() and not leak memory. If you want it automated, you can always use garbage collection.

author: Nominal Animal
author_rank: Super Contributor
timestamp: February 19, 2022, 11:03:49 am
content: The reason I pointed at MMU-less Linux kernels is that the Linux kernel uses dynamic memory allocation for basically everything. Yet, it is pretty darned stable; as in, if you have a solid power supply, and you don't need to reboot because of userspace issues, you can haveyearsof continuous uptime.Userspace code is different; it is rarely written with the same kind of care and careful design. (And those of us who rail against the devs that happily create such crappy code and push it into basically all Linux distributions with big paid social efforts [by paid community managers hired by the devs' employers like Red Hat], get called "anti-whateverzealots" and "haters".)Virtual memory, and memory management units that do not provide virtual memory but do provide a kernel-userspace boundary stopping userspace processes from messing with kernel memory or memory belonging to other userspace processes, provide a way for the kernel to isolate the memory used by each process. That way, when those userspace processes exit or crash, the kernel can recover every single byte used by that exact process, and do so in a safe and robust manner.This is not any kind of an indication that an MMU is required for stable operation. It only means that most userspace code is written so sloppily, that one needs an MMU to stop that sloppily written code from crashing the entire system. Realising this is important.As such, the fact that there were MMU-less Linux appliances that were stable over timescales from months to years, indicates that dynamic memory management itself does not automatically mean instability or unreliability. The fact that there were many MMU-less Linux appliances that were unstable, does not prove or even indicate that the reason is dynamic memory management. These facts mean that stability without an MMU seems to be something many programmers cannot achieve, and dynamic memory management is just one of these things that confuse a lot of C programmers.All this circles back to my statement: that which is technically possible and achievable, may not be what is realistic with the given resources. But that is due to human limitations, not due to technical limitations.

author: nctnico
author_rank: Super Contributor
timestamp: February 19, 2022, 11:16:52 am
content: Quote from: Nominal Animal on February 19, 2022, 11:03:49 amAn MMU has nothing to do with dynamic memory allocation working well or not. The primary function of an MMU is to provide seperation between processes, not to manage the amount of memory. This also touches the subject of security. Without an MMU to shield processes from eachother, every process basically runs as root.The PowerPC based Macs (Apple) didn't have an MMU as well. Back in the day that Macs had PowerPC processors the difference with Windows (which uses protected memory) was very clear. A poorly behaving program would crash a Mac completely (full stop) while on Windows only the offending process got shut down.When you need high availability, software needs to be stable either way (with or without MMU).

author: brucehoult
author_rank: Super Contributor
timestamp: February 19, 2022, 08:16:21 pm
content: Quote from: nctnico on February 19, 2022, 11:16:52 amCompletely untrue. Every PowerPC used in a Mac had an MMU. PowerPC came from IBM, where it ran AIX, a Unix. PowerPC Macs could run completely normal Linux using their MMUs. G3, G4, and G5 PowerPC Macs ran Mac OS X when it was released, using their MMUs.Pre-PowerPC Macs using the 68030 and 68040 processors also had MMUs.MacOS simply didn't *use* the MMU to protect the OS from apps or apps from each other.QuoteTrue, but Windows still crashed more than Macs at the time.I had a Mac SE/30 used as a server which several times ran for 18 months without a reboot.

author: westfw
author_rank: Super Contributor
timestamp: February 19, 2022, 10:57:04 pm
content: QuoteIn other words, a system with an MMU and short-lived applications can survive those applications doing a really bad job of dynamic memory management. "How do you make sure you've freed all the memory you allocated?" "exit(0)!"The MMU doesn't help if the "application" are long-lived, or the memory allocations are in non-application code.

author: nctnico
author_rank: Super Contributor
timestamp: February 19, 2022, 11:10:23 pm
content: Quote from: westfw on February 19, 2022, 10:57:04 pmNo. In the end the OS keeps track of how much and which memory is allocated by a process and can free it after a process stops. The MMU doesn't play a role in that because the MMU doesn't know how much memory actually belongs to a process (think about the swap space).

author: westfw
author_rank: Super Contributor
timestamp: February 20, 2022, 12:13:51 am
content: QuoteA paging MMU assigns pages of memory to a process "below" malloc() level (sbrk?), and cleanup means returning all those pages to the free pool, right? An OS doesn't keep track of things at the malloc() level. I guess it's a thin line whether you want to consider that page management separate from dynamic allocation.Are there OS's without MMU that keep track of dynamically allocated memory on a per-process level and free it all when the process ends? (huh. Much of the dynamic allocation I've done in MMU-less systems specifically uses dynamic allocation to create data structures that outlive the process that created them...)

author: nctnico
author_rank: Super Contributor
timestamp: February 20, 2022, 12:48:39 am
content: Quote from: westfw on February 20, 2022, 12:13:51 amMy understanding is that a process gets an amount of heap space from which it can malloc memory. The entire heap space can be freed once the process is done (on Linux it seems it depends on the amount of memory requested whether heap or system memory is used where system memory is being tracked using a file descriptor). The MMU only knows how to map a virtual address on a real memory address (and throw an exception if that virtual address isn't part of any real memory and thus needs to be swapped by the OS).Either way, the OS needs to keep track of which memory is allocated by what process. How else can it process a call to free() and know how much memory needs to be freed / how much memory can be used for malloc calls after free?

author: brucehoult
author_rank: Super Contributor
timestamp: February 20, 2022, 02:24:31 am
content: Quote from: nctnico on February 20, 2022, 12:48:39 amAh ha. Proof positive that you don't know what you're talking about. No shame in that, everyone starts from that position and has to learn, but it does explain some of your weirder statements in this thread.Here's your chance to learn :-)An OS such as Linux, Windows, MacOS knows nothing at all about free(). That's a C runtime library thing.An application that wants to be nice to the OS when it allocates and frees large blocks of memory will use system calls mmap() (CreateFileMappingA() on Windows) and munmap() (or possibly madvise()) instead of C library functions malloc() and free().If you're lucky, free() might also use madvise(), but it can only do that for entire VM pages -- and that might be either ignored or counter-productive if the OS is using megapages or gigapages to cut down on TLB misses.

author: DavidAlfa
author_rank: Super Contributor
timestamp: February 20, 2022, 02:38:53 am
content: Even in microcontrollers, malloc is super useful.Your program might have different execution states, each one using different buffers, or ex. rarely used firmware update mode that needs a very large buffer.Without dynamic allocation you would need to statically allocate everything (Which might not fit) or use buffer recycling taking care that nothing overlaps or overflows.With malloc you don't have to deal with all that, adding a new system execution mode would be very easy, all you need is to free up the old memory and allocate the new one.Has its issues, like fragmentation, but can be minimized with some tweaking, ex. allocating small segments first.

author: nctnico
author_rank: Super Contributor
timestamp: February 20, 2022, 02:48:22 am
content: Quote from: brucehoult on February 20, 2022, 02:24:31 amI know all that; I just don't want to pull all kinds of technicalities in. From an application's perspective the OS starts at the C runtime library. You can ofcourse start a whole semantic discussion about what is what. Linux is actually just the kernel but most use the word Linux to point to the whole collection of files that make a system do something useful. Where do you put the division between where the kernel ends and where the OS begins?The whole point is that the MMU doesn't know about how much memory a process is using and thus is not relevant for using dynamic memory allocation working well or not.

author: brucehoult
author_rank: Super Contributor
timestamp: February 20, 2022, 03:32:40 am
content: Quote from: nctnico on February 20, 2022, 02:48:22 amNo it doesn't.If for no other reason than many applications don't use the C runtime library but instead some other runtime library, or the syscall interface directly.Even for applications written in C it is common for a sophisticated application to replace/override portions of the C runtime library *especially* malloc() and free() with customised implementations such as jemalloc, dlmalloc, boehm etc.QuoteAt the syscall interface.QuoteThe MMU doesn't know anything much at all. It's simply a small circuit implementing an associative mapping between a couple of dozen or a few hundred virtual page numbers and a physical page number (or "not present") and access rights.The kernel uses the MMU as a mechanism to implement policy.

author: Nominal Animal
author_rank: Super Contributor
timestamp: February 21, 2022, 02:31:20 am
content: Amemory management unit, MMU, performs two unrelated functions:Virtual memory mappingMemory protectionThe former is useful, but not necessary. The latter, memory protection, is what allows userspace processes to crash without affecting other processes, and which also allows the kernel to reliably track which memory regions were allocated for that processes, and therefore reap them when that process exits or crashes.Systems without an MMU at all cannot stop userspace processes from accessing kernel memory, if they do not have memory protection. In systems with an MMU, userspace processes cannot directly access kernel memory.This memory protection is what allows a kernel to operate in a stable and robust manner, even when userspace processes have bugs and memory leaks. Note "allows": it does require the kernel memory management code is carefully written, so that it does not leak memory or lose track of memory when errors or unexpected situations occur.Without memory protection, a buggy userspace process can accidentally overwrite crucial kernel memory structures, and crash the entire machine.(Several microcontrollers have partial memory protection. Certain memory-mapped registers, for example, can often be "locked" against accidental modification, by writing a certain value to a certain locking register.)There have been examples of MMU-less Linux machines running carefully written userspace processes, providing useful services, both the kernel and the userspace processes using dynamic memory management, with only a few megabytes of RAM total, for months on end without any issues. This is proof that it can be done, and is only a matter of skill and care.With MMUs, carelessly written userspace processes no longer crash the entire machine, only that particular process. In an ideal world, programmers would have kept the care they needed to take before MMUs were commonplace; but instead, what happened, was that programmers instead started to care less. After all, if the process crashed, you only needed to restart it to keep going; it became"no big deal".Similarly, when available RAM was measured in bytes, at best kilobytes, programmers took care to use as little as they needed.When the amount of RAM available increased, instead of keeping to the memory-frugal patterns, programmers started to use the available RAM to make development easier and faster, instead of passing on the gains as-is to the users.You need to run e.g. Microsoft Word 6 on PowerMac 7200 running Mac OS 7.5 at 75 MHz with 8 MiB of RAM, to understand what is possible. These machines do have MMUs, but the OS also used an indirect pointer scheme for certain RAM resources, and even allowed the user to trivially specify the amount of RAM each application could use at runtime (from the application properties context menu). Compare that to todays desktop machines running todays Word or LibreOffice Writer, on a processor thirty times faster (but with multiple parallel cores available), with hundreds of times the amount of RAM. The actual human-observed performance is very similar. Which is quite depressing, really.I, and several other members here, are perfectly capable of using dynamic memory management even on microcontrollers. It is not so hard you need an especially clever person to do that; all you need is to learn the skill to do that, and practice a lot so you can avoid the typical pitfalls when doing so.However, that skill is not something most employers look for. Indeed, my own skills are not in high demand at all, because while I write quite reliable code, I am slower ("less productive") than others: I keep insisting on making sure my code is reliable before I "publish" it, and this is not desirable from the employers' point of view. It is better to push code as soon as possible, since code stability and reliability is not a selling point. In fact, it is desirable for the product to have bugs and defects, because fixing themin a subsequent versionacts as an incentive for paying customers to upgrade.

author: brucehoult
author_rank: Super Contributor
timestamp: February 21, 2022, 04:43:26 am
content: Quote from: Nominal Animal on February 21, 2022, 02:31:20 amNote that most currently for sale RISC-V microcontrollers implement M and U execution modes and have PMP (Physical Memory Protection) which allows RWX permissions to be granted to U mode software in up to typically 8 or 16 defined memory address ranges.M mode software can quickly change the PMP settings when switching from one U mode program to another. PMP settings, once made, can also be locked against any further changes (even by M mode) until a system reset.The main chips that don't have PMP are the GD32VF103 in the Longan Nano, early FE310-G000 in the original HiFive1 (the FE310-G002 and -G003 have User mode and PMP), and the K210 (which has a full (old 1.9.1 specification) MMU but not PMP).ARM also has a similar optional feature called MPU.I don't think PIC and AVR have anything similar? At least I haven't noticed it.

author: Nominal Animal
author_rank: Super Contributor
timestamp: February 21, 2022, 05:22:17 am
content: No, I don't recall any AVRs that have any memory protection, either. (Some, like AT90USB1286, have lock bits related to access to Flash memory, with Flash memory divided into Boot loader region and Application region, and locking separately controlled for each region; and some might have lock registers for memory-mapped peripherals; but I do not recall ever seeing anything involving RAM regions, no.)In another thread here at EEVBlog forums I mentioned that I wished AVRs (and other microcontrollers) had a stack pointer extremum register, and a stack pointer limit register with a software interrupt if the stack pointer grows past that value. It would make runtime heap-stack collision detection very robust, in the cases where heap and stack are a single continuous memory region with one growing up and the other growing down.

author: nctnico
author_rank: Super Contributor
timestamp: February 21, 2022, 02:44:50 pm
content: Quote from: Nominal Animal on February 21, 2022, 05:22:17 amI doubt that is possible. When I look at ARM assembly code I see indexing relative to the stack pointer. So even though the stack pointer may still be in a valid range, the data addressed is not. What you could do is position the stack at the start of the memory instead of at the end. Once the stack overflows you'll get a memory or bus error (at least on ARM). That way the stack can never grow into your variable or heap space.

author: Nominal Animal
author_rank: Super Contributor
timestamp: February 21, 2022, 03:27:23 pm
content: Quote from: nctnico on February 21, 2022, 02:44:50 pmEven if it were, it is extremely unlikely to be implemented anyway.For compiled code, it is possible to instrument function preambles to provide those in software. Anyway, it is just one way one could solve the problem of checking whether the available RAM suffices for both the stack and the heap.

author: dietert1
author_rank: Super Contributor
timestamp: February 21, 2022, 05:22:07 pm
content: As far as i remember IAR supports stack usage supervision in debug executables. They fill the stack with a pattern and count how many of the "protection pattern" is left. Remember complaints when i implemented a similar method at application level - with my own pattern..I never made an application that uses the stack for dynamic memory in the sense the heap is meant to be used. Maybe it makes sense, as dynamic memory is for data to be used in a certain application context or state. Heap is necessary for multitasking where the context changes rapidly and without completion.Regards, Dieter

author: SiliconWizard
author_rank: Super Contributor
timestamp: February 21, 2022, 06:29:36 pm
content: Quote from: brucehoult on February 21, 2022, 04:43:26 amThe PIC32MZ do, AFAIR.

author: brucehoult
author_rank: Super Contributor
timestamp: February 21, 2022, 08:35:01 pm
content: Quote from: SiliconWizard on February 21, 2022, 06:29:36 pmThat's 32 bit MIPS and an ARM/RISC-V equivalent. I'd think most people who say "PIC and AVR" are talking about 8 bit, and I certainly was.

author: tellurium
author_rank: Frequent Contributor
timestamp: July 03, 2022, 08:49:11 am
content: Quote from: nctnico on February 21, 2022, 02:44:50 pmCan you elaborate on that please? I don't understand how that would work.My understanding of heap and stack growing towards one another as an optimal solution for a single contiguous block of memory which is eaten by two different allocation mechanisms (malloc/sbrk and push/pop), where it is not known in advance which one is used more. To use a memory block to its full extent, we position two allocation mechanisms on the opposite ends of memory block and let them grow towards one another. An OOM condition happens when brk heap marker meets the stack pointer register, hence the suggestion to detect that condition on a hardware level, which makes sense to me.If we position both heap and stack at the beginning of the memory block, how stack allocation is going to work then? Would malloc and push/pop clash regardless which direction stack grows? Or am I missing something?

author: emece67
author_rank: Frequent Contributor
timestamp: July 03, 2022, 11:23:03 am
content: .

author: brucehoult
author_rank: Super Contributor
timestamp: July 03, 2022, 11:34:07 am
content: Quote from: emece67 on July 03, 2022, 11:23:03 amA distinct lack of flexibility there for Cortex-M -- no way to have a "red zone" below the stack pointer, as used by powerpc and x86_64 ABIs, so that leaf functions usually don't have to adjust the SP even if they spill some values to the stack.

author: peter-h
author_rank: Super Contributor
timestamp: July 03, 2022, 07:47:07 pm
content: QuoteThe arm32 uses a different stack for interrupts.This question arose in connection with FreeRTOS stacks. Don't theyallhave to be big enough for interrupt servicing? With say 20 tasks that would be a lot of memory wasted. The answer is NO; a different stack is used for ISRs i.e. the automatic context save for interrupts switches the SP to another stack.I don't know any more details.QuoteIn terms of memory addresses, the stack is normally at the top of memory and grows downwards. Then, starting from the bottom of memory, initialised data, static variables, etc, are allocated. The top of the static stuff is the base of the heap, which grows upwards from there. The heap must not meet the bottom of the stack, and a function called sbrk() is used to tell malloc() whether the next request is allowed. This is my one:Code:caddr_t _sbrk(int incr){extern char end asm("end");extern char top asm("_top");static char *heap_end;char *prev_heap_end;// This test is never met since heap_end is never going to be = 0// "end" = 0x2000edf8 (see linkfile)if (heap_end == 0)heap_end = &end;prev_heap_end = heap_end;// top = 2001e000 (top of 128k RAM minus 8k stack)if (heap_end + incr > &top){//write(1, "Heap and stack collision\n", 25);//abort();errno = ENOMEM;return (caddr_t) -1;}heap_end += incr;return (caddr_t) prev_heap_end;}There is a different version of _sbrk going around, which works, until it doesn'thttps://community.st.com/s/question/0D50X0000AAIEWbSQP/stm32cubemx-generated-sbrk-in-syscallsc-not-working-with-freertosBut I may have everything backwards; people here know way more than I doRe LWIP, it has various memory models. The one I am using is where you give it a static block to use and it runs its internal heap inside that. Apparently this is supposed to be deterministic because when the connection is closed, that area is all cleaned up.I would never use a heap in any code I write, other than for grabbing some RAM for optional product features which are started and then never terminate all the time the thing is powered up.I would like to know which C standard lib functions use malloc() internally. One tends to assume that printf() and its variantsdoes, which will create havoc with any RTOS (fragmentation, or outright crashes unless you mutex malloc and free etc) unless you restrict the printf() usage to just one thread. I would certainly hope that sscanf() does not use the heap. This sitehttps://nadler.com/embedded/newlibAndFreeRTOS.htmlsuggests that printf does not use the heap unless you are outputting floats.

author: emece67
author_rank: Frequent Contributor
timestamp: July 03, 2022, 11:00:49 pm
content: .

author: emece67
author_rank: Frequent Contributor
timestamp: July 03, 2022, 11:26:16 pm
content: .

author: ejeffrey
author_rank: Super Contributor
timestamp: July 04, 2022, 12:24:44 am
content: Quote from: nctnico on February 15, 2022, 11:53:26 amThat's not the point. Bruce is assuming this is potentially the only use of dynamic memory in the system. The point is that if you preallocate the array you have to bound both the number and size of values. If you use dynamic allocation you can use a greater number of small values while allowing storage of an occasional large item. Even with a fixed amount of storage dedicated to this data structure you use it more efficiently with dynamic allocation -- assuming the overhead of dynamic allocation is less than the savings from having more accurate allocation and that you can deal with fragmentation.Of course either way you need to handle out of memory.

author: peter-h
author_rank: Super Contributor
timestamp: July 04, 2022, 06:13:45 am
content: QuoteWhere is this enabled?QuoteWhat is meant by "mbed"?QuoteYes; you should never store data below the stack, because (in any "normal" system) interrupts will corrupt it. So local variables in a function are stored on a stack frame which sitsabovethe SP.QuoteOne can prevent heap going too high (because _sbrk checks the requested block size against the predeclared heap top) but the only way to prevent stack going too far down is some sort of hardware trap, but that assumes the said memory will get accessed. It might just get allocated, and perhaps accessed much laterBTW is there a utility which can display the heap?

author: brucehoult
author_rank: Super Contributor
timestamp: July 04, 2022, 07:28:39 am
content: Quote from: peter-h on July 04, 2022, 06:13:45 amPowerPC and x86_64 are therefore not "normal" systems, because their Linux/Mac ABIs all allow functions to use up to 128 bytes of stack space *below* the stack pointer.Microsoft Windows similarly allows the use of up to 16 bytes below SP on Itanium, 8 bytes on 32 bit ARM, and 16 bytes on 64 bit ARM. But not on x86.Consider :Code:int foo(){int a;return a;}x86_64: (https://godbolt.org/z/8ajP3PoGP)Code:foo():mov eax, DWORD PTR [rsp-44]retpowerpc:Code:.L.foo():lwa 3,-36(1)blr This is of course UB, but change the code to, say, the following and exactly the same thing happens, SP is not adjusted and local variable a[] is stored below SP.Code:int foo(){int a;a = 0; a = 1;for (int i=2; i<10; ++i)a = a[i-1] + a[i-2];return a;}

author: emece67
author_rank: Frequent Contributor
timestamp: July 04, 2022, 07:33:55 am
content: .

author: peter-h
author_rank: Super Contributor
timestamp: July 04, 2022, 07:54:15 am
content: QuoteI am not as clever as youbut I can't see this. The whole array a is stored on the stack of that function, and SP points at the return address - otherwise the function could not return to the caller.QuoteThanks. I guess FreeRTOS must set this bit when it starts up. I had a dig around and found thisCode:/**\brief Union type to access the Control Registers (CONTROL).*/typedef union{struct{uint32_t nPRIV:1; /*!< bit: 0 Execution privilege in Thread mode */uint32_t SPSEL:1; /*!< bit: 1 Stack to be used */uint32_t FPCA:1; /*!< bit: 2 FP extension active flag */uint32_t _reserved0:29; /*!< bit: 3..31 Reserved */} b; /*!< Structure used for bit access */uint32_t w; /*!< Type used for word access */} CONTROL_Type;but CONTROL_Type or SPSEL are not referenced other than in .h files.I can't find where this bit is set. In Cube debug, among the registers is CONTROL and the value in that, with RTOS running, is 100 so SPSEL=0.

author: brucehoult
author_rank: Super Contributor
timestamp: July 04, 2022, 09:57:36 am
content: Quote from: peter-h on July 04, 2022, 07:54:15 amThe array a is stored in the stack region of memory, but BELOW the stack pointer, hence the x86 code returning the value at [SP-44] and the PowerPC code returning the value at -36(r1). (It's just a difference of alignment)QuoteOn x86 yes. On PowerPC the return address is not in RAM but in a register.

author: peter-h
author_rank: Super Contributor
timestamp: July 04, 2022, 12:23:05 pm
content: QuoteLearn something every dayThat cannot possibly work with interrupts unless the ISR auto-switches to a different stack.Regarding the two stack pointers, MSP and PSP, I am seeing reasonable values in both of these, and checked that they are both moving about the right way in both ISRs and RTOS tasks, but nowhere can I see SPSEL=1 actually set.It is possible that the debugger generates an interrupt in which case the SPSEL bit gets cleared, so always shows as 0 when stepping. However I also can't find where it might be set to 1. For example herehttps://github.com/FreeRTOS/FreeRTOS-Kernel/blob/23f641850d2428eac3e164d6e735e6e92dc3914a/portable/GCC/ARM_CM4F/port.c#L256they explicitly load 0 into it, and sure enough my own code does the sameCode:static void prvPortStartFirstTask( void ){/* Start the first task. This also clears the bit that indicates the FPU isin use in case the FPU was used before the scheduler was started - whichwould otherwise result in the unnecessary leaving of space in the SVC stackfor lazy saving of FPU registers. */__asm volatile(" ldr r0, =0xE000ED08\n" /* Use the NVIC offset register to locate the stack. */" ldr r0, \n"" ldr r0, \n"" msr msp, r0\n" /* Set the msp back to the start of the stack. */" mov r0, #0\n" /* Clear the bit that indicates the FPU is in use, see comment above. */" msr control, r0\n"" cpsie i\n" /* Globally enable interrupts. */" cpsie f\n"" dsb\n"" isb\n"" svc 0\n" /* System call to start first task. */" nop\n");}The above loads the MSP and I found the code which also loads the PSPCode:void vPortSVCHandler( void ){__asm volatile ("ldrr3, pxCurrentTCBConst2\n" /* Restore the context. */"ldr r1, \n" /* Use pxCurrentTCBConst to get the pxCurrentTCB address. */"ldr r0, \n" /* The first item in pxCurrentTCB is the task top of stack. */"ldmia r0!, {r4-r11, r14}\n" /* Pop the registers that are not automatically saved on exception entry and the critical nesting count. */"msr psp, r0\n" /* Restore the task stack pointer. */"isb\n""mov r0, #0\n""msrbasepri, r0\n""bx r14\n""\n"".align 4\n""pxCurrentTCBConst2: .word pxCurrentTCB\n");}but nowhere can I find where it sets SPSEL=1

author: brucehoult
author_rank: Super Contributor
timestamp: July 04, 2022, 01:38:19 pm
content: Quote from: peter-h on July 04, 2022, 12:23:05 pmSure it can, and does.I don't know what they do with x86_64, but with PowerPC, Itanium and some others the interrupt routine simply decrements the stack pointer by 128 bytes before starting to push the return address and processor status and other registers.What puzzles me, if anything, is why the RISC-V ABI didn't also adopt this common pattern of a "Red Zone" on the stack.

author: tellurium
author_rank: Frequent Contributor
timestamp: July 04, 2022, 02:08:42 pm
content: Quote from: brucehoult on July 04, 2022, 01:38:19 pmMaybe to save a precious 128 bytes of memory , keeping in mind embedded use cases?

author: peter-h
author_rank: Super Contributor
timestamp: July 04, 2022, 03:46:03 pm
content: Very interesting thread.Apart from the remaining 32F417 + FreeRTOS mystery of where SPSEL=1 gets set, it is pretty obvious that the heap is pretty useless in an RTOS environment. Even if you mutex malloc() and free() your system will eventually blow up due to fragmentation. So the heap can be used from onlyoneRTOS task, or perhaps from others if you can make sure there is no time overlap and the allocated memory is never freed.

author: ejeffrey
author_rank: Super Contributor
timestamp: July 04, 2022, 04:28:23 pm
content: Quote from: peter-h on July 04, 2022, 03:46:03 pmIt's neither obvious nor true, and there are several ways to deal with fragmentation. The most generic is to use memory handles which was very common in systems without virtual memory. Pool allocators are useful for transaction processing where you might need to allocate several small pieces of memory for a task but then free them all a once when the task is complete. You can use power of two allocators with pools for each size to limit fragmentation.QuoteAlso not true. You really have to not think of "heap / dynamic allocations" as "randomly and carelessly calling malloc all over the place." For instance you can have multiple fixed memory pools, one per thread/task. You can preallocate memory for a particular data structure and then dynamically assign subunits as needed. For instance a network stack might have a fixed memory allocated for the whole stack and dynamically allocate packet buffers from it.There are many RTOS applications where dynamic allocation is unnecessary and best avoided. And it's likely none of these approaches will work for the smallest memory systems. But there are plenty of applications for dynamic memory in embedded systems.

author: peter-h
author_rank: Super Contributor
timestamp: July 04, 2022, 04:41:36 pm
content: QuoteSure; this is what LWIP does. It is also what FreeRTOS does although that does it only once, as tasks are created.That is what I meant when referring to allocating memory for optional features; memory which is never freed. LWIP frees up its private heap when the connection is closed, AIUI.But private heaps are a bad example because the original block isstaticallyallocated, with a worst-case size.QuoteIf you do a malloc() of say 2048 bytes, doesn't it grab slightly more than 2048 (due to it being a linked list)? What units does one have to run to avoid fragmentation?

author: emece67
author_rank: Frequent Contributor
timestamp: July 04, 2022, 05:42:14 pm
content: .

author: peter-h
author_rank: Super Contributor
timestamp: July 04, 2022, 07:39:37 pm
content: Ah yes... very clever

author: eutectique
author_rank: Frequent Contributor
timestamp: July 04, 2022, 09:01:09 pm
content: Quote from: peter-h on July 04, 2022, 03:46:03 pmHere:Code:void vPortSVCHandler( void ){__asm volatile (" ldr r3, pxCurrentTCBConst2 \n"/* Restore the context. */" ldr r1,  \n"/* Use pxCurrentTCBConst to get the pxCurrentTCB address. */" ldr r0,  \n"/* The first item in pxCurrentTCB is the task top of stack. */" ldmia r0!, {r4-r11, r14} \n"/* Pop the registers that are not automatically saved on exception entry and the critical nesting count. */" msr psp, r0 \n"/* Restore the task stack pointer. */" isb \n"" mov r0, #0 \n"" msr basepri, r0 \n"" bx r14 \n"" \n"" .align 4 \n""pxCurrentTCBConst2: .word pxCurrentTCB \n");}ldmialoads LR with0xfffffffd. Thenbx r14returns to Thread mode and PSP is used.

author: brucehoult
author_rank: Super Contributor
timestamp: July 05, 2022, 01:07:19 am
content: Quote from: tellurium on July 04, 2022, 02:08:42 pm"Embedded" covers a very wide range these days!These days "embedded" covers things such as Pi compute modules (and RISC-V equivalents mostly with the Allwinner D1 chip at the moment) with 256 MB to a few GB DRAM.Sure, a lot of popular AVRs and PICs have only 512 bytes or 2 K of RAM (and some have much less), but I don't recall seeing a RISC-V core that has -- or even that can be optioned with -- less than 16 KB DTIM. Even at that level a 128 byte buffer between main program stack and an interrupt handler is less than 1% of RAM. Popular embedded chips such as the ESP32-C3 (400 KB) or Bouffalo BL602 (276 KB) have a lot more, let alone the Kendryte K210 with 8 MB.Also, there can be more than one ABI.The original (or "Linux") RISC-V ABI is designed to maximise compute speed in the main program code. Having (as is quite standard these days) a mix of caller-save and callee-save registers allows many leaf functions (those with only scalar local variables, and no more than 15 of them) to not have to build a stack frame at all. A "red zone" extends that to functions with a small number of local structs and arrays. It's only two instructions saved, but every little bit helps.There can also be a specialised "embedded" ABI. Some people have been calling for an ABI with less latency on entering interrupt handlers, by only having to save maybe 6 or 7 registers before entering C code (like Cortex M) instead of 15. A proposal has been in the works for a few years now. I did some work on it in 2019 when I was at SiFive. For example, you might reduce the number of argument registers from 8 to 4 (the same as ARM) and reduce the number of temporary registers from 7 to maybe 2. You could also have the "embedded" ABI not have a Red Zone even if the Linux ABI does.Actually, I suspect the "embedded" ABI might never be made official. The loud voices that wanted one became quite unhappy when they learned that reducing the number of registers that needed to be saved on an interrupt causes so many more register spills and reloads to the stack during normal execution that it reduces the speed of main program code by 20%. Yes guys, the standard ABI was designed to maximise the speed of the main program -- if you tinker with it then you get slower, by definition. (Unless it was done badly in the first place)Also, "__attribute_((interrupt))" was added to the C compiler. A function with this decoration saves only as many registers as it will actually use. The full set or registers are saved only if and when it calls out to a C function that doesn't have the interrupt attribute.And this basically solves the problem of interrupt handlers that are so short that saving 15 registers instead of 6 makes a significant difference. In fact it's better, because a simple "__attribute__((interrupt))" function might save only 1 or 2 or 3 registers.

author: brucehoult
author_rank: Super Contributor
timestamp: July 05, 2022, 01:34:39 am
content: Quote from: ejeffrey on July 04, 2022, 04:28:23 pmIt certainly makes sense to have a small number of different sizes, and round each allocation up to the next size. Power of two might be a bit extreme. I've done memory managers for what are essentially embedded software (mobile phones with a few hundred KB of RAM, old 6502 and z80 machines with 64 KB or less). Often having blocks of 1.0 and 1.5 times a power of two works well e.g. 8, 12, 16, 24, 32, 48, 64, 96... In my experience, going more fine-grained than that gets counter-productive.When such a block is freed, it goes on to a list of free blocks of that size, and can be immediately reused.Usually I create groups of small blocks by subdividing a large block of maybe 256 bytes or 1K or 4K (fixed, depending on total RAM size) into equal sized small blocks. Then, if an entire large block of small blocks becomes free, it can be reallocated for a different block size if needed. Sorting free blocks of a given size can promote this happening (maybe just a heap, not a full sorted list).Many systems have periodic memory needs. Often there is a startup phase that is very distinct from the running phase, so it is good to reclaim as much memory as possible at the end of startup. It is good if you can get the application programmer to call a special memory management function at this point.Pools are good for periodic allocation and mass freeing when the task doing that can be easily separated from other things happening at the same time (or only one thing happens at a time).One mistake many memory managers have historically made is a large block being freed, and then the memory manager splits it up to service smaller requests. e.g. "first fit" or "worst fit" algorithms. If the demand is periodic then a large block of the same size will very likely be needed soon, and if the original has been split up then a new large block must be found. Repeated many times this can result in a large number of small, unneeded, unusable memory blocks but no contiguous space for a large block to be allocated.Real memory managers stopped making that mistake decades ago, but alas not people rolling their own by following their old university textbook!Another thing I do when I have full control over the whole program is to simply forbid individual memory allocations over a certain size. On a machine with a few hundred KB of RAM that might be as small as 256 bytes. On a machine with hundreds of MB to a couple of GB it might be 4 KB (or the same as the VM/TLB page size). If the programmer wants an array bigger than that then they have to use an array of pointers to blocks, or a tree of blocks, or a list. I provide classes for arrays and strings and buffers that do this internally, without complicating the external interface.

author: brucehoult
author_rank: Super Contributor
timestamp: July 05, 2022, 01:38:32 am
content: Quote from: peter-h on July 04, 2022, 04:41:36 pmOnly if "it" is stupid. Don't use stupid software.Competent people writing things such as power of two allocators store metadata for the blocks somewhere else.Block metadata tends to be the same size regardless of the block size, so can be conveniently stored in an array-like structure somewhere.There are also advantages because many operations only need to look at or modify the metadata, not the data block itself, so keeping the metadata together reduces cache misses, TLB misses, page faults ...

author: dmsc
author_rank: Newbie
timestamp: July 05, 2022, 04:19:53 am
content: Hi!Quote from: brucehoult on July 04, 2022, 01:38:19 pmx86 is always over-complicated: in 32 bit mode, interrupts are searched in the IDT (interrupt-descriptor-table), where the address (and segment) of the interrupt handler and the protection level is specified, then the stack pointer (and the stack segment) is loaded from the TSS (the task-state-segment, a table with the stacks available for each switch from one protection level to another) if specified. This is one of the reasons that interrupt latency is that big on x86.In 64 bit mode, there is a new IST (interrupt-stack-table) that can be used to load stacks for interrupts with less indirection.

author: peter-h
author_rank: Super Contributor
timestamp: July 05, 2022, 06:12:04 am
content: QuoteThat means the malloc() function needs an array allocated and that sets a limit on how many blocks can be in use. Or does it use its own allocation mechanism for the array, growing it as necessary?

author: brucehoult
author_rank: Super Contributor
timestamp: July 05, 2022, 11:06:04 am
content: Quote from: peter-h on July 05, 2022, 06:12:04 amOf course, if it is competently written.There are many different malloc() libraries in the world.