C strcat, strcpy and armageddon, part 3

See also: part 1 and part 2.

Previously, I discussed a way to make string copies safer by using strncpy(). I mentioned there would be a bit of extra overhead and I’d like to discuss that. This made me wonder: how much overhead? I decided to try and find out.

First, I created a very simple test program that copied a string using strcpy(), or strncpy() (with the extra null added).


#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define BUFSIZE 20

//#define SMALL

int main()
{
char buffer[BUFSIZE];

#ifdef SMALL
// Smaller and faster, but less safe.
strcpy(buffer, "Hello");
#else
// Larger and slower, but more safe.
strncpy(buffer, "Hello", BUFSIZE-1);
buffer[BUFSIZE-1] = '\0';
#endif

// If you don't use 'buffer', it may be optimized out.
puts(buffer);

return EXIT_SUCCESS;
}




Since I am lazy, I didn't want to make two separate test programs. Instead, I used a #define to conditionally compile which version of the string copy code I would use.

When I built this using GCC for Windows (using the excellent Code::Blocks editor/IDE), I found that each version produced a .exe that was 17920 bytes. I expect the code size difference might start showing up after using a bunch of these calls, so this test program was not good on a Windows compiler.

Instead, I turned to the Arduino IDE (currently version 1.6.7). It still uses GCC, but since it targets a smaller 16-bit AVR processor, it creates much smaller code and lets me see size differences easier. I modified the code to run inside the setup() function of an Arduino sketch:


#define BUFSIZE 10
#define SMALL

void setup() {
// volatile to prevent optimizer from removing it.
volatile char buffer[BUFSIZE];

#ifdef SMALL
// Smaller and faster, but less safe.
strcpy((char)buffer, "Hello");
#else
// Larger and slower, but more safe.
strncpy((char)buffer, "Hello", BUFSIZE-1);
buffer[BUFSIZE-1] = '\0';
#endif
}

void loop() {
// put your main code here, to run repeatedly:
}




Then I selected Sketch Verify/Compile (Ctrl-R, or the checkmark button). Here are the results:

  • SMALL (strcpy) - 540 bytes
  • LARGE (strncpy) - 562 bytes

It seems moving from strcpy() to strncpy() would add only 22 extra bytes to my sketch. (Without the "buffer[BUFSIZE-1] = '';" line, it was 560 bytes.)

Now, this does not mean that every use of strncpy() is going to add 20 bytes to your program. When the compiler links in that library code, only one copy of the strncpy() function will exist, so this is more of a "one time" penalty. To better demonstrate this, I created a program that would always link in both strcpy() and strncpy() so I could then test the overhead of the actual call:


#define BUFSIZE 10
#define SMALL

void setup() {
// volatile to prevent optimizer from removing it.
volatile char buffer[BUFSIZE];

// For inclusion of both strcpy() and strncpy()
strcpy((char)buffer, "Test");
strncpy((char)buffer, "Test", BUFSIZE);

#ifdef SMALL
// Smaller and faster, but less safe.
strcpy((char)buffer, "Hello");
#else
// Larger and slower, but more safe.
strncpy((char)buffer, "Hello", BUFSIZE-1);
//buffer[BUFSIZE-1] = '\0';
#endif
}

void loop() {
// put your main code here, to run repeatedly:
}




Now, with both calls used (and trying to make sure the optimizer didn't remove them), the sketch compiles to 604 bytes for SMALL, or 610 bytes for the larger strncpy() version. (Again, without the "buffer[BUFSIZE-1] = '';" line it would be 608 bytes.)

Conclusions:

  1. The strncpy() library function is larger than strcpy(). On this Arduino, it appeared to add 20 bytes to the program size. This is a one-time cost just to include that library function.
  2. Making a call to strncpy() is larger than a call to strcpy() because it has to deal with an extra parameter. On this Arduino, each use would be 4 bytes larger.
  3. Adding the null obviously adds extra code. On this Arduino, that seems to be 2 bytes. (The optimizer is probably doing something. Surely it takes more than two bytes to store a 0 in a buffer at an offset.)

Since the overhead of each use is only a few bytes, there's not much of an impact to switch to doing string copies this safer way. (Assuming you can spare the extra 20 bytes to include the library function.)

Now we have a general idea about code space overhead, but what about CPU overhead? strncpy() should be slower since it is doing more work during the copy (checking for the max number of characters to copy, and possibly padding with null bytes).

To test this, I once again used the Arduino and it's timing function, millis(). I created a sample program that would do 100,000 string copies and then print how long it took.


#define BUFSIZE 10
//#define SMALL

void setup() {
// volatile to prevent optimizer from removing it.
volatile char buffer[BUFSIZE];
unsigned long startTime, endTime;

Serial.begin(115200); // So we can print stuff.

// For inclusion of both strcpy() and strncpy()
strcpy((char)buffer, "Test");
strncpy((char)buffer, "Test", BUFSIZE);

// Let's do this a bunch of times to test.
startTime = millis();

Serial.print("Start time: ");
Serial.println(startTime);

for (unsigned long i = 0; i < 100000; i++)
{
#ifdef SMALL
// Smaller and faster, but less safe.
strcpy((char)buffer, "Hello");
#else
// Larger and slower, but more safe.
strncpy((char)buffer, "Hello", BUFSIZE - 1);
buffer[BUFSIZE - 1] = '\0';
#endif
}
endTime = millis();

Serial.print("End time  : ");
Serial.println(endTime);

Serial.print("Time taken: ");
Serial.println(endTime - startTime);
}

void loop() {
// put your main code here, to run repeatedly:
}




When I ran this using SMALL strcpy(), it reports taking 396 milliseconds. When I run it using strncpy() with the null added, it reports 678 milliseconds. strcpy() appears to take about 60% of the time strncpy() does, at least for this test. (Maybe. Math is hard.)

Now, this is a short string that requires strncpy() to pad out the rest of the buffer. If I change it to use a 9 character string (leaving one byte for the null terminator):

#ifdef SMALL
    // Smaller and faster, but less safe.
    strcpy(buffer, "123456789");
#else
    // Larger and slower, but more safe.
    strncpy(buffer, "123456789", BUFSIZE - 1);
    buffer[BUFSIZE-1] = '';
#endif

...no padding will be done. Without padding, the SMALL version takes 572 and the strncpy()/null version takes... 478!?!

Huh? How can this be? How did the "small" version suddenly get SLOWER? Well, before, strcpy() only had to copy the five characters of "Hello" plus a null then it was done, while strncpy() had to copy "Hello" then pad out five nulls to fill the buffer. Once both had to do the same amount of work (copying nine bytes and a null), it appears that strncpy() is actually faster! (Your mileage may vary. Different compilers targeting different processors may generate code in vastly different ways.)

Perhaps there is just some optimization going on when the destination buffer size is know. (Note to self: Look in to the GCC strncpy source code and see what it does versus strcpy.)

Conclusion:

  • strncpy() isn't necessarily going to be slower (at least on this Arduino)!
  • strncpy() might be significantly slower if you copy a very short string ("Hi") in to a very long buffer (char buffer[80];).

Buyer Programmer beware!

I am sure glad we (didn't) clear that up. In the next part, I'll get back to talking about appending strings using strcat() and how to make that safer.

To be continued...

3 thoughts on “C strcat, strcpy and armageddon, part 3

  1. Pingback: C strcat, strcpy and armageddon, part 6 | Sub-Etha Software

  2. Pingback: C strcat, strcpy and armageddon, part 4 | Sub-Etha Software

  3. Pingback: C strcat, strcpy and armageddon, part 5 | Sub-Etha Software

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.