MAWK-ARRAYS(7)
Okay bestie, gather round, because this is mawk-arrays(7) from the Miscellaneous section ๐ง๐ It's basically the design diary for how mawk stores awk arrays under the hood, and it is lowkey iconic.
โจ NAME โจ
mawk-arrays โ design notes for mawk's array implementation ๐
โจ SYNOPSIS โจ
This is the documentation for the mawk implementation of awk arrays ๐ In awk, arrays are associations of strings to awk scalar values, and mawk stores those associations in hash tables ๐ฆ The hash table scheme was influenced by, and is similar to, the design in Griswold and Townsend, The Design and Implementation of Dynamic Hashing Sets and Tables in Icon, Software Practice and Experience, 23, 351-367, 1993. Very academic, very cute ๐
โจ DATA STRUCTURES โจ
๐ธ Array Structure ๐ธ
The type ARRAY is a pointer to a struct array ๐ซถ The size field is the number of elements in the table. The meaning of the other fields depends on the type field.
There are three types of arrays, and they're told apart by the type field in the structure. Here they are:
AY_NULL๐ค The array is empty, and thesizefield is always zero. The other fields have no meaning, so don't go looking for vibes in them ๐AY_SPLITโ๏ธ The array was created by the AWK built-insplit. The return value fromsplitis stored in thesizefield. Theptrfield points at a vector ofCELLs, and the number ofCELLs is thelimitfield. It is always true thatsize โค limit. The address ofA[i]is(CELL*)A->ptr+i-1for1โค i โค size. Thehmaskfield has no meaning here, so it's not giving anything ๐- Hash Table ๐ชฃ The array is a hash table. If the
AY_STRbit in thetypefield is set, the table is keyed on strings. If theAY_INTbit in thetypefield is set, the table is keyed on integers. Both bits can be set, and then the two keys are consistent, i.e., looking upA[-14]andA["-14"]will return identicalCELLpointers, even though the lookup methods are different ๐ In this case, thesizefield is the number of hash nodes in the table. When inserting a new element would causesizeto exceedlimit, the table grows by doubling the number of hash chains. The invariant(hmask+1)max_ave_list_length=limitis always true.Max_ave_list_lengthis a tunable constant, so it's giving adjustable ๐ง
๐ง Hash Tables ๐ง
The hash tables are linked lists of nodes, called ANODEs ๐ The number of lists is hmask+1, which is always a power of two. The ptr field points at a vector of list heads. Since there are potentially two types of lists, integer lists and string lists, each list head is a structure, DUAL_LINK ๐
The string lists are chains connected by slinks, and the integer lists are chains connected by ilinks. We sometimes call these lists slists and ilists, respectively ๐ The elements on the lists are ANODEs. The fields of an ANODE are:
slinkโ the link field for slists ๐ilinkโ the link field for ilists ๐svalโ if non-null, thensvalis a pointer to a string key. For a given table, if theAY_STRbit is set, then everyANODEhas a non-nullsvalfield, and conversely, ifAY_STRis not set, then everysvalfield is null. Main character energy ๐ญhvalโ the hash value ofsval. This field has no meaning ifsvalis null.ivalโ the integer key. The field has no meaning if it is set to the constantNOT_AN_IVALUE. If theAY_STRbit is off, then everyANODEwill have a validivalfield. If theAY_STRbit is on, then theivalfield may or may not be valid, so it's a little unpredictable ๐cellโ the data field in the hash table.\ndhitemsโจ
So the value of A[expr is stored in the cell field ๐ฏ If expr is an integer, then expr is stored in ival; otherwise, it is stored in sval. And that's the whole storage glow-up ๐ซง
โจ ARRAY OPERATIONS โจ
okay so bestie, here are the functions that run all the array stuff ๐
CELL* array_find(ARRAY A, CELL *cp, int create_flag)
this one returns a pointer to A[expr], where cp points at the CELL holding expr. if create_flag is on and expr isn't already an element of A, it gets created with value null. โจ
void array_delete(ARRAY A, CELL *cp)
removes A[expr from the array A ๐๏ธ and cp points at the CELL holding expr.
void array_load(ARRAY A, size_t cnt)
builds a split array ๐ the values A[1..cnt] get moved into A from an anonymous buffer using transfer_to_array(), which is declared in split.h.
void array_clear(ARRAY A)
removes all elements of A, and afterwards the type of A is AY_NULL. clean slate, periodt ๐ซง
STRING** array_loop_vector(ARRAY A, size_t *sizep)
this returns a pointer to a linear vector holding all the strings that are indices of A ๐ the size of the vector comes back indirectly in *sizep. and if A->sizeโก0, you get a null pointer back. ๐
CELL* array_cat(CELL *sp, int cnt)
this one concatenates the elements of sp[1-cnt..0], with each element separated by SUBSEP, to build an array index. for example, on a reference to A[i,j], array_cat computes i โ SUBSEP โ j, where โ means concatenation. it's giving math-class, but it's actually so simple ๐ฆ
๐ Array Find
okay so any reference to A[expr] makes a call to array_find(A,cp,CREATE), where cp points at the cell holding expr. and the test, expr in A, makes a call to array_find(A,cp,NO_CREATE). ๐ง
Array_find is a hash-table lookup function, and it handles two cases:
- if
*cpis numeric and integer valued, it looks up by integer value usingfind_by_ival. if*cpis numeric but not integer valued, it converts to a string withsprintf(CONVFMT,...)and goes to case 2. - if
*cpis string valued, it looks up by string value usingfind_by_sval.\ndlist
to check whether cp->dval is an integer, we round it toward zero to the nearest integer (that's done by do_to_I), then cast it back to double. if we get the same number we started with, then cp->dval is integer valued. the math is lowkey genius ๐ค
once we reach find_by_ival, the search has already been narrowed down to a lookup in a hash table by integer value. ๐
when an integer search fails, we have to check by string value too, so that inserting A["123"] and later searching A[123] still works. this string search is necessary if and only if the AY_STR bit is set. and here's the important bit: every ANODE gets created with a valid sval when AY_STR is set, because then creating new nodes always happens in a call to find_by_sval. ๐
searching by string value is way easier, because AWK arrays are really just string associations ๐ซถ if the array doesn't have the AY_STR bit set, we have to convert it to a dual hash table with strings, and that's done by the function add_string_associations.
one Int value is reserved to show that the ival field is invalid. this works because d_to_I returns a value in [-Max_Int, Max_Int]. โจ
on entry to add_string_associations, we already know the AY_STR bit is not set. so we convert to a dual hash table, then walk all the integer lists and put each ANODE onto a string list. main character energy ๐
๐๏ธ Array Delete
the statement delete A[expr] makes a call to array_delete(ARRAY A, CELL *cp). depending on the type of *cp, the call gets routed to either find_by_sval or find_by_ival. each of those leaves its return value on the front of an slist or ilist (respectively), and then that node gets deleted from the front of the list. ๐ซง
the case where A[expr is on two lists, like A[12] and A["12"], is checked by looking at the sval and ival fields of the returned ANODE*. ๐
even though we found a node by searching an ilist, it might also be on an slist, and vice-versa. wild, I know ๐ญ
when the size of a hash table drops below a certain value, it might be profitable to shrink the table. but currently we don't do this, because our guess is it would be a waste of time for most AWK applications ๐ข however, we do convert an array to AY_NULL when its size goes to zero. that resizes a big hash table that got fully cleared by a bunch of deletions.
๐ Building an Array with Split
a simple operation is creating an array with the AWK primitive split. the code that does the split puts the pieces into an anonymous buffer, and then array_load(A, cnt) moves the cnt elements from that buffer into A. this is the only way an array of type AY_SPLIT gets created. ๐ธ
if the array A is already a split array and it's big enough, we reuse it. otherwise we allocate a new split array. when we allocate a block of CELLs for a split array, we round up to a multiple of 4. tidy ๐งน
๐ซง Array Clear
the function array_clear(ARRAY A) converts A to type AY_NULL and frees all the storage A used, except for the struct array itself. this function gets called in three contexts:
- when an array local to a user function goes out of scope,
- when the AWK statement
delete Agets executed, - when an existing array changes type or size from
split().
๐๏ธ Constructor and Conversions
arrays are always created as empty arrays of type AY_NULL. global arrays are never destroyed, although they can go empty or change type through conversion. the only constructor function is a macro ๐
hash tables only get constructed by conversion, and that happens in two ways. the function make_empty_table converts an empty array of type AY_NULL into an empty hash table. the number of lists in the table is a power of 2, determined by the constant STARTING_HMASK. the limit size of the table is determined by the constant MAX_AVE_LIST_LENGTH, which is the largest average size of the hash lists we're willing to put up with before the table gets enlarged. when A->size exceeds A->limit, the hash table grows by doubling its number of lists. then A->limit gets reset to MAX_AVE_LIST_LENGTH times A->hmask+1. ๐
the other way a hash table gets constructed is when a split array gets converted into a hash table of type AY_INT. ๐งโโ๏ธ
to figure out the size of the table, we set the initial size to STARTING_HMASK+1, then keep doubling until A->size โค A->limit. ๐
๐ Doubling the Size of a Hash Table
the whole point of making the table size a power of two is to make resizing easy ๐ฆ if the table size is 2**(n) and h is the hash key, then h mod 2**(n) is the hash chain index, which can be calculated with bit-wise and, h & (2**(n-1)). when the table size doubles, the new bitmask has one more bit turned on. elements of an old hash chain whose hash value has that bit turned on get moved to a new chain, and elements with the bit turned off stay on the same chain. on average, only half the old chain moves. if the old chain is at table[i], where 0 โค i < 2**(n), then the elements that move all go to the new chain at table[i + 2**(n)]. ๐
as we walk an old string list with pointer p, the expression p->hval & new_hmask takes one of two values. if it's equal to p->hval & old_hmask (which equals i), the node stays put. otherwise it gets moved to a new string list at j. the new string list keeps its order, so the positions of the move-to-the-front heuristic are preserved. nodes moving to the new list get appended at the pointer tail. the ANODEs dummy0 and dummy1 are sentinels, which means we don't need special handling for boundary conditions. clever, right? ๐ง
the doubling of the integer lists works exactly the same way, except slink gets replaced by ilink and hval gets replaced by ival. ๐
๐ Array Loops
our mechanism for running the statement
for (i in A) { statements }
is simple. we allocate a vector of STRING* of size A->size. each element of the vector is a string key for A. note that if the AY_STR bit of A isn't set, then A has to be converted to a string hash table, because the index i walks string indices. ๐ซถ
to execute the loop, the only state we need to save is the address of i and an index into the vector of string keys. since nothing about A is saved as state, the user program can do anything to A inside the loop body, even delete A, and the loop still works. ๐ญ basically we traded data space (the string vector) for implementation simplicity. on a 32-bit system, each ANODE is 36 bytes, so the extra memory the array loop needs is 11 more than the memory consumed by the ANODEs of the array. the big ANODEs are a sign of our whole design, which pays data space for integer lookup speed and algorithm simplicity. ๐ธ
the only part of array loops that lives in array.c is building the string vector. the rest of the implementation is in the file execute.c. ๐
as we walk over the hash table ANODEs and put each sval into ret, we need to increment each reference count. the user of the return value is responsible for those new reference counts. ๐ฅ
๐ Concatenating Array Indices
in AWK, an array expression A[i,j] is equivalent to the expression A[i SUBSEP j]. that means the index is the concatenation of the three elements i, SUBSEP and j. this is performed by the function array_cat. on entry, sp points at the top of a stack of CELLs. cnt cells get popped off the stack, concatenated together separated by SUBSEP, and the result gets pushed back on the stack. on entry, the first multi-index is in sp[1-cnt] and the last is in sp[0]. the return value is the new stack top. ๐ธ
the stack here is the run-time evaluation stack. this operation really has nothing to do with array structure, so logically this code belongs in execute.c, but it stays here for historical reasons. ๐
we make a copy of SUBSEP, which we can cast to string in the unlikely event the user has assigned a number to SUBSEP. ๐
set sp and top so the cells to concatenate are inclusively between sp and top.
the total_len is the sum of the lengths of the cnt strings and the cnt-1 copies of subsep. ๐งฎ
the return value is sp, and it's already set correctly. we just need to free the strings and set the contents of sp. and that's the whole tea โ
Version 1.3.4 ๐ธ dated 2024-01-23, and that's a wrap on MAWK-ARRAYS(7)! you did amazing sticking with all this, bestie ๐ซถ๐๐ฆ
๐ฆ where this page comes from
- alpine 3.22: mawk-doc 1.3.4-r11 ยท GPL-2.0-only
- alpine 3.23: mawk-doc 1.3.4_p20250131-r0 ยท GPL-2.0-only
- alpine 3.24: mawk-doc 1.3.4_p20260302-r0 ยท GPL-2.0-only
- debian trixie: mawk 1.3.4.20250131-1 ยท GPL-2.0-only
- epel 10: mawk 1:1.3.4-4.20250131.el10_1 ยท GPL-2.0-only
- epel 9: mawk 1:1.3.4-4.20250131.el9 ยท GPL-2.0-only
- fedora 43: mawk 1:1.3.4-5.20250131.fc43 ยท GPL-2.0-only
- fedora 44: mawk 1:1.3.4-6.20260129.fc44 ยท GPL-2.0-only
- fedora 44: mawk 1:1.3.4-7.20260302.fc44 ยท GPL-2.0-only
- nixos 26.05: mawk 1.3.4-20240819
- nixpkgs unstable: mawk 1.3.4-20240819
- opensuse-leap 16.0: mawk 1.3.4.20240622-bp160.1.12 ยท GPL-2.0-only
- opensuse-leap 16.0: mawk 1.3.4.20240622-bp160.1.12 ยท GPL-2.0-only
- opensuse-leap 16.0: mawk 1.3.4.20240622-bp160.1.12 ยท GPL-2.0-only
- opensuse-leap 16.0: mawk 1.3.4.20240622-bp160.1.12 ยท GPL-2.0-only
- ubuntu noble: mawk 1.3.4.20240123-1build1 ยท GPL-2.0-only
- ubuntu questing: mawk 1.3.4.20250131-1 ยท GPL-2.0-only
- ubuntu resolute: mawk 1.3.4.20260129-1 ยท GPL-2.0-only
- ubuntu stonking: mawk 1.3.4.20260302-1 ยท GPL-2.0-only
- copyright 2008-2020,2024, Thomas E. Dickey
- copyright 1996, Michael D. Brennan
- ๐ girlified by anthropic/claude-haiku-5.5 on : a language model's rewrite of the original, under its terms