Wanted to also give a small input. I had to implement a very similar function for a project of mine. I can't disclose the algorithm, however. But let me try to give you a couple small hints as to how you could further improve yours. I'm foremost concerned with stability and performance (low latency and high throughput) of code.
Let's start with this (profiled for a 1024-byte long string for 10,000 iterations with gprof):
Each sample counts as 0.01 seconds.
% cumulative self self total
time seconds seconds calls us/call us/call name
67.16 0.02 0.02 10000 2.01 2.01 randstring
33.58 0.03 0.01 10000 1.01 1.01 mkrndstr
0.00 0.03 0.00 10000 1.01 1.01 mkrndstrperf
The difference is here really thin as on a few repetitions gprof gives random data, so let's take callgrind instead.
The total cost is then more precisely calculated in function calls:
7,986,402,268 ???:randstring [~perf/t/mkrndstr-notmine]
6,655,222,307 ???:mkrndstr [~perf/t/mkrndstr-notmine]
6,653,436,779 ???:mkrndstr_ipa [~perf/t/mkrndstr-notmine]
6,653,436,778 ???:mkrndstr_ipd [~perf/t/mkrndstr-notmine]
For a 10-byte long string and 10,000 calls, the relative difference is even bigger:
9,968,042 ???:randstring [~perf/t/mkrndstr-notmine]
8,646,775 ???:mkrndstr [~perf/t/mkrndstr-notmine]
6,716,774 ???:mkrndstr_ipa [~perf/t/mkrndstr-notmine]
6,716,774 ???:mkrndstr_ipd [~perf/t/mkrndstr-notmine]
Here, mkrndstr_ip{a,d}
means: in-place for automatically stored and dynamically stored data (different calls, identical functions), respectively.
Some key take-aways beforehand:
down-casting
size_t l = (sizeof(charset) -1); // uncast
versus
int l = (int)(sizeof(charset) -1); // cast to int as suggested in William Morris' reply
makes on that scale a big difference--you avoid passing around loads of superfluous bytes.
The static set of chars is a good idea, saves many cycles and calls, I'd prefix it with a const qualifier.
It's a bad idea to do per-cycle/per-iteration instantiations and identical calculations for the same reasons as in 2 above, make the value, loosely speaking, sticky as Quonux demonstrates it.
Considering randomness. I guess you know libc's rand()
is an implementation of a PRNG. It's useless for anything serious. If your code gets executed too quickly, i.e., when there's too little interval between any two successive calls to rand()
, you'll get the same character from the set. So make sure to pause for a couple CPU cycles. You could also simply read chunks from /dev/urandom
(with the u
prefix, otherwise my "simply" wouldn't hold) or similar on a UNIX derivative. Don't know how to access the random device on Windows.
strlen
is indeed slower than sizeof
for clear reasons (expects complex strings), see the implementation in glibc sources, for example, in string/strlen.c
.
The rest is in the comments.
Let's get to the code:
Yours, final:
char *randstring(size_t length) { // length should be qualified as const if you follow a rigorous standard
static char charset[] = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789,.-#'?!";
char *randomString; // initializing to NULL isn't necessary as malloc() returns NULL if it couldn't allocate memory as requested
if (length) {
randomString = malloc(length +1); // I removed your `sizeof(char) * (length +1)` as sizeof(char) == 1, cf. C99
if (randomString) {
for (int n = 0;n < length;n++) {
int key = rand() % (int) (sizeof(charset) -1);
randomString[n] = charset[key];
}
randomString[length] = '\0';
}
}
return randomString;
}
Mine (only slight optimizations):
char *mkrndstr(size_t length) { // const size_t length, supra
static char charset[] = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789,.-#'?!"; // could be const
char *randomString;
if (length) {
randomString = malloc(length +1); // sizeof(char) == 1, cf. C99
if (randomString) {
int l = (int) (sizeof(charset) -1); // (static/global, could be const or #define SZ, would be even better)
int key; // one-time instantiation (static/global would be even better)
for (int n = 0;n < length;n++) {
key = rand() % l; // no instantiation, just assignment, no overhead from sizeof
randomString[n] = charset[key];
}
randomString[length] = '\0';
}
}
return randomString;
}
Now considering your question regarding dynamic versus automatic data storage:
void mkrndstr_ipa(size_t length, char *randomString) { // const size_t length, supra
static char charset[] = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789,.-#'?!";
if (length) {
if (randomString) {
int l = (int) (sizeof(charset) -1);
for (int n = 0;n < length;n++) {
int key = rand() % l; // per-iteration instantiation
randomString[n] = charset[key];
}
randomString[length] = '\0';
}
}
}
There's also an identical function with the modified name as stated way above. Both are called like this:
char *c = malloc(SZ_STR +1); // dynamic, in C on the heap
char d[SZ_STR +1]; // "automatic," in C on the stack
mkrndstr_ipd(SZ_STR, c);
mkrndstr_ipa(SZ_STR, d);
- Here's a short note on memory allocation in C++
- Here's a more visual note
Now the more interesting part:
.globl randstring
.type randstring, @function
randstring:
.LFB0:
.cfi_startproc
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
pushq %rbx
subq $40, %rsp
.cfi_offset 3, -24
call mcount
movq %rdi, -40(%rbp)
cmpq $0, -40(%rbp)
je .L2
movq -40(%rbp), %rax
addq $1, %rax
movq %rax, %rdi
call malloc
movq %rax, -24(%rbp)
cmpq $0, -24(%rbp)
je .L2
movl $0, -28(%rbp)
jmp .L3
.L4:
call rand
movl %eax, %ecx
movl $1991868891, %edx
movl %ecx, %eax
imull %edx
sarl $5, %edx
movl %ecx, %eax
sarl $31, %eax
movl %edx, %ebx
subl %eax, %ebx
movl %ebx, %eax
movl %eax, -32(%rbp)
movl -32(%rbp), %eax
imull $69, %eax, %eax
movl %ecx, %edx
subl %eax, %edx
movl %edx, %eax
movl %eax, -32(%rbp)
movl -28(%rbp), %eax
movslq %eax, %rdx
movq -24(%rbp), %rax
addq %rax, %rdx
movl -32(%rbp), %eax
cltq
movzbl charset.1808(%rax), %eax
movb %al, (%rdx)
addl $1, -28(%rbp)
.L3:
movl -28(%rbp), %eax
cltq
cmpq -40(%rbp), %rax
jb .L4
movq -40(%rbp), %rax
movq -24(%rbp), %rdx
addq %rdx, %rax
movb $0, (%rax)
.L2:
movq -24(%rbp), %rax
addq $40, %rsp
popq %rbx
popq %rbp
.cfi_def_cfa 7, 8
ret
.cfi_endproc
.LFE0:
.size randstring, .-randstring
compared with:
.globl mkrndstr
.type mkrndstr, @function
mkrndstr:
.LFB1:
.cfi_startproc
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
subq $48, %rsp
call mcount
movq %rdi, -40(%rbp)
cmpq $0, -40(%rbp)
je .L7
movq -40(%rbp), %rax
addq $1, %rax
movq %rax, %rdi
call malloc
movq %rax, -8(%rbp)
cmpq $0, -8(%rbp)
je .L7
movl $69, -16(%rbp)
movl $0, -12(%rbp)
jmp .L8
.L9:
call rand
movl %eax, %edx
sarl $31, %edx
idivl -16(%rbp)
movl %edx, -20(%rbp)
movl -12(%rbp), %eax
movslq %eax, %rdx
movq -8(%rbp), %rax
addq %rax, %rdx
movl -20(%rbp), %eax
cltq
movzbl charset.1818(%rax), %eax
movb %al, (%rdx)
addl $1, -12(%rbp)
.L8:
movl -12(%rbp), %eax
cltq
cmpq -40(%rbp), %rax
jb .L9
movq -40(%rbp), %rax
movq -8(%rbp), %rdx
addq %rdx, %rax
movb $0, (%rax)
.L7:
movq -8(%rbp), %rax
leave
.cfi_def_cfa 7, 8
ret
.cfi_endproc
.LFE1:
.size mkrndstr, .-mkrndstr
I believe an interpretation is not necessary here, the disassembly is then only for visualisation. Compare the different number of instructions.
What I wanted to show is that just a couple of tiny nip'n'tucks does wonders. We sure could go memmove the string into an external buffer at a fixed address or do n-byte-wise assignments. We could also put the set in a macro or use even more static allocation. But let's not exaggerate and go write this outright in ASM, and when we'd be done with it, do the same in pure machine code.
I hope this helps and if it's not a real-time system, then don't do much hassle with the optimizations, stability and reliability (like rand()
) should go first. If you optimize this and that out, something else somewhere else could break. There are a few open-source projects online that show how to optimize excessively but at the cost of an unmaintanable code for newcomers to their teams and sheer complexity for the more accustomed developers.
Last thing I'd like to point you at is that
error: ‘for’ loop initial declarations are only allowed in C99 mode