In awk scripts, I see several methods used to concatenate an array of strings with a separator, mainly trinary being used in some way, or the separator is changed on the fly. Occasionally printf/sprintf is used but I won't consider that here as I'd be surprised if the function call is not expensive.
For example, given an awk array a of strings, with integer indices from 1 to max, and separator sep:
trinary:
j=""
for ( i=1; i<=max; i++ )
j = ( j ? j sep a[i] : a[i] )
modify:
j=s=""
for ( i=1; i<=max; i++ ) {
j = j s a[i]
s = sep
}
I have just come up with this method which I don't recall seeing before (ed: from subsequent comments, this turns out to be similar to a method used by awklib):
mine (for):
j = a[1]
for ( i=1; i<max; )
j = j sep a[++i]
Naively, I would assume this to be more efficient as:
- there are no tests other than for the loop counter;
- there are no modifications to the separator; and
- the redundant final test is elided.
Another option I just thought of:
mine (while):
j=a[i=max]
while (i>1)
j = a[--i] sep j
Running a simple benchmark of gawk and mawk on a mostly idle Ubuntu 20.04 laptop (9 trials each of 1,000,000 iterations of joining a 64-element array of 50-char strings) gave me:
gawk
| algorithm | min | max | median | mean |
|---|---|---|---|---|
| trinary | 13.863 | 15.214 | 14.148 | 14.330 |
| modify | 11.052 | 11.926 | 11.360 | 11.468 |
| mine (for) | 8.612 | 8.792 | 8.698 | 8.710 |
| mine (while) | 12.267 | 13.263 | 12.591 | 12.706 |
mawk
| algorithm | min | max | median | mean |
|---|---|---|---|---|
| trinary | 12.406 | 13.299 | 12.827 | 12.806 |
| modify | 12.975 | 13.744 | 13.085 | 13.294 |
| mine (for) | 11.641 | 12.397 | 12.233 | 12.151 |
| mine (while) | 8.400 | 9.083 | 8.693 | 8.693 |
busybox awk is much less performant. Using 100,000 iterations:
| algorithm | min | max | median | mean |
|---|---|---|---|---|
| trinary | 11.004 | 12.284 | 11.784 | 11.605 |
| modify | 11.708 | 12.628 | 11.879 | 12.071 |
| mine (for) | 10.695 | 11.642 | 10.776 | 11.021 |
| mine (while) | 9.244 | 10.167 | 9.409 | 9.549 |
original-awk is faster than busybox in this situation. Using 250,000 iterations:
| algorithm | min | max | median | mean |
|---|---|---|---|---|
| trinary | 12.750 | 13.525 | 12.840 | 13.014 |
| modify | 13.105 | 13.778 | 13.942 | 13.572 |
| mine (for) | 11.831 | 12.543 | 12.710 | 12.281 |
| mine (while) | 10.730 | 10.854 | 11.466 | 11.032 |
I guessed my second method might be even faster due to not needing to dereference max in the loop but, intriguingly, although it runs noticably faster than the other algorithms in most implementations, it runs poorly in gawk, where my first method is the clear winner.
Are there even better methods?
Is there a best idiom for awk join?
My benchmark code was just:
for v in gawk mawk "busybox awk" original-awk; do
for m in 1 2 3 4; do
echo ::: $v $m :::
for n in 1 2 3 4 5 6 7 8 9; do
time </dev/null $v -f aw$m-${v:0:1}
done
echo
done
done
aw3-b: (others are similar)
BEGIN {
a[ 1] = "a[ 1]a[ 1]a[ 1]a[ 1]a[ 1]a[ 1]a[ 1]a[ 1]a[ 1]a[ 1]"
a[ 2] = "a[ 2]a[ 2]a[ 2]a[ 2]a[ 2]a[ 2]a[ 2]a[ 2]a[ 2]a[ 2]"
# ... more lines ...
a[63] = "a[63]a[63]a[63]a[63]a[63]a[63]a[63]a[63]a[63]a[63]"
a[64] = "a[64]a[64]a[64]a[64]a[64]a[64]a[64]a[64]a[64]a[64]"
max=64
sep=FS
for(n=1; n<=100000; n++) {
j=a[1]
for (i=1;i<max;)
j = j sep a[++i]
}
}