Static Buffers vs Dynamically Allocated Buffers

My code sends out UDP and TCP Packets of varying lengths depending on the situation. The packet / buffer size varies from 10 bytes to 150 bytes.

As of now I dynamically initialize a uint8_t send_buffer[10] or uint8_t send_buffer[150] and fill them up with the data that I want to send within a function.

Since I know that the max size of any buffer is 150 bytes, is it better to have a global buffer initialized right at the beginning with a fixed size of 150 bytes and use the same buffer for all situations OR have the buffers created dynamically as I need them?

Example:

uint8_t global_send_buffer[150];

// need to send out 10 bytes
udp.write(global_send_buffer, 10);

// need to send out 150
udp.write(global_send_buffer, 150);

The answer to this question largely depends on your use of the buffer(s).

Just two possible hints

  • multiple messages that will be reused -> dynamic
  • messages are virtually unique and will never be reused -> static
2 Likes

Hi @nitred

I can’t tell if you are using automatic variables (often called stack allocated) in a function or if you are doing something like

uint8_t * buf;
buf = new uint8_t[10]; 
//or later
buf = new uint8_t[150];

If you are calling new then I think you will eventually run out of memory. The garbage collector will not be able to completely de-fragment this over time and it doesn’t take too many holes to run you out of RAM

I would think that a static 150 byte array that you manage would be much better in the long run, but as @ScruffR said it depends a lot on the lifetime of your data. Also, if a portion of the data is the same every time, you can probably optimize that further.

1 Like

@bko Here’s my simple use case. The code is similar to what I actually use. When I receive a udp packet, I decode it and send an appropriate response (send_function()) back. I have about 10 different / unique send_functions() each with a different buffer size.

void send_function_a(){  
uint8_t send_buffer[10] = "a:";
    for(uint8_t i=2; i<10; i++){
        send_buffer[i] = i;
    }
    udp.write(send_buffer, 10);  // i have left out udp.beginPacket() and udp.endPacket()
}

void send_function_b(){
    uint8_t send_buffer[75] = "b:";
    for(uint8_t i=2; i<75; i++){
        send_buffer[i] = i;
    }
    udp.write(send_buffer, 75);
}

void send_function_c(){
    uint8_t send_buffer[150] = "c:";
    for(uint8_t i=2; i<150; i++){
        send_buffer[i] = i;
    }
    udp.write(send_buffer, 150);
}



void loop(){
    if(udp.available()){
        uint8_t recv = udp.read();            // receive a udp message
        if(recv == 'a') {                     // decode the message
            void send_function_a();
        }
        else if(recv == 'b'){
            void send_function_b();
        }
        else if ...
        else if ...
        else{
            void send_function_c()
        }
    }
}

So I don’t use new. I think I’m using what you called automatic variables. @ScruffR I’d like to have your views as well. Thanks!

This is for long deployment so I really want to have minimum memory issues and really don’t mind losing out on slight performance.

Hi @nitred

A very clever compiler could figure out via variable lifetime analysis that all of your send_buffer automatic variables could share storage, but in the real world the analysis is not always perfect. It might to some sharing, you would have to check the compiler output (compiling on your local machine) carefully.

I think you would be better off in general to have one global send_buffer[150] and use it in all three functions. Using strcpy or memcpy you can put the different preambles into the buffer in each function.


uint8_t send_buffer[150];
const uint8_t preamble_a = { 'a', ':' };  // only in flash
//etc
void send_function_a() {
  memcpy(send_buffer, preamble_a, 2);
  for(...)
}

If you switch from 8-bits (uint8_t) in the future, the size you give to memcpy will have to be computed differently, but you get the idea.

Because you always passing the length to udp.write() you never need to clear the buffer–just overwrite it with good data where you need to.

Edit: @bko beat me again, with a completely differen view of the matter :wink:

@nitred, since I can’t see any functional difference in your functions I’d go for only one function, that’ll take parameters (e.g. category (=recv) or category plus buffsize) inside the fn you’d use a local (automatic) buffer which can be sized for the max size (which I’d #define rather than hardcode inside the fn) and then use the parameters to choose between different behaviours.

This way you safe space twice. Local variables don’t take up any space until the fn is called and will vanish once the fn finishes and you safe space since the “shared” code does not exist multiple times as it would do with different functions.

Advantage of Brians version is, that your buffer is created once and for all and so is better performance wise.
And if it happened that the fn gets called recursifly, you will not run out of stack, as you would with my suggestion.

@bko Thank you so much. I can only see a positive outcome moving in this direction.

@ScruffR The actual functions in my code are pretty unique in functionality and do get called recursively, I just made them simple here. I’m looking for absolute stability so I think I’ll have to go with @bko’s suggestions here :smile: Thanks a lot though!