/** \file \internal \page Hashed + Indexed Access to Metadata Objects \tableofcontents The original internal representations of metadata in memory relied on linear searching of lists to locate various objects by name or by numeric id: by _varid_ or by _grpid_ for example. In recent years, the flaws in that approach have become obvious as users create files with extremely large numbers of objects: groups, variables, attributes, and dimensions. One case has 14 megabytes of metadata. Creating and (especially) later opening such files was exceedingly slow. This problem was partially alleviated in both netcdf-3 (libsrc) and netcdf-4 (libsrc4) by adding name hashing tables. However, and especially for netcdf-4, linear search still prevailed. A pervasive change has been made to try to change searches by name or by id from O(n) to O(1). This uses hashing for name-based search and vectors for numeric id-based search. All other cases were left as O(n) searches. This document describes the architecture and details of the netCDF internal object lookup mechanisms now in place. \section Sindexed_searches Indexed Searches There are, as a rule, two searches that are used to locate metadata objects: (1) search by name and (2) search by externally visible id (e.g. dimid or varid). It is currently the case that after all the metadata is read or created, hashing is used for locating objects by name. In all other cases -- apparently -- lookup is by linear search of a of linked list. It is relevant that, once created, no metadata object -- except attributes -- can be deleted. They can be renamed, but that does not change the associated structure or id. Deletion only occurs when an error occurs in creating an object or on invoking "nc_close()". The numeric identifiers for dimensions, types, and groups are all globally unique across a file. But note that variable id's are not globally unique (IMO a bad design decision) but are only unique within the containing group. Thus, in order to provide a unique id for a variable it must be composed of the containing group id plus the variable id. Note also that names are unique only within a group and with respect to some kind of metadata. That is a group cannot have e.g. two dimensions with the same name. But it can have a variable and a dimension with the same name (as with coordinate variables). Finally, attribute names are unique only with respect to each other and with respect to the containing object (a variable or a group). \section Sbasic_data_structures Basic Data Structures The basic data structures used by the new lookup mechanisms are described in the following sections. \subsection Snclist NClist With rare exceptions, vectors of objects are maintained as instances of NClist, which provides a dynamically extendible vector of pointers: pointers to metadata objects in this case. It is possible to append new objects or insert at a specific vector offset, or overwrite an existing pointer at a specific offset. The NClist structure definition is as follows. \code typedef struct NClist { size_t alloc; size_t length; void** content; } NClist; \endcode \subsection Snc_exhashmap NCexhashmap The NCexhashmap type is a hash table mapping a key to a data item. As a rule, the data item is a pointer to a metadata object. This hash map uses Fagin's extendible hashing algorithm. The current implementation supports table expansion as described for that algorithm. The hashtable definition is as follows. \code typedef struct NCexhashmap { int depth; /* Global depth */ int nactive; /* # of active entries in whole table */ NCexleaf** directory; /* |directory| == 2^depth */ ... } NCexhashmap; \endcode where the directory (of leaf pointers) has (1<children; g; g = g->l.next) { if(strcmp(name,g->name)==0) { ... code to process matching grp by name } ... } \endcode In this case, this loop is iterating across all the subgroups (children) of the grp. It does so by walking the linked list of child groups. It does a name comparison in order to find the group with the desired name. In the new code, this iteration cliche is replaced by something that looks like this. \code NC_GRP_INFO_T* grp = ...; NC_GRP_INFO_T* g; ... g = ncxindexlookup(grp->children,name); if(g != NULL) ... code to process matching grp by name } \endcode In this case, the iteration is replaced by a hashtable lookup. \subsection Slookupid Lookup an Object by id In the original code, the following _cliche_ (code snippet) was common for looking up an object by its id (dimid, varid, etc). \code NC_GRP_INFO_T* grp = ...; ... NC_DIM_INFO_T* d; ... for (d = grp->dim; d; d = d->l.next) { if(varid == d->dimid) ... code to process matching dim by index } ... } \endcode In this case, this loop is iterating across all the dimension objects of the grp. It does so by walking the linked list of dimensions. It does an id comparison in order to find the group with the desired dimension. In the new code, this iteration cliche is replaced by something that looks like this. \code NC_FILE_INFO_T* h5 = ...; NC_DIM_INFO_T* d;; ... d = nclistget(h5->alldims,id); if(d != NULL) ... code to process matching dim by id } \endcode This shows how the alldims vector is used to map from a dimid directly to the matching dimension object. In this example, h5 is the NC_FILE_INFO_T file object. This approach works for dimension ids, group ids, and type ids because they are globally unique. For variables and attributes, we have to use the containing group's Ncxindex, such as grp->vars. In this case, the varid, is mapped using code like this. \code NC_GRP_INFO_T* grp = ...; NC_VAR_INFO_T* v; ... v = ncxindexith(grp->vars,id); if(v != NULL) ... code to process matching variable by id } \endcode \subsection Siterate Iterating over sets of objects In the original code, the following _cliche_ (code snippet) was common. \code NC_GRP_INFO_T* grp; ... NC_GRP_INFO_T* g; ... for (g = grp->children; g; g = g->l.next) ... \endcode In this case, this loop is iterating across all the subgroups (children) of the grp. It does so by walking the linked list of child groups. Similar loops are used to walk a list of dimensions, variables, types, or attributes. In the new code, this iteration cliche is replaced by something that looks like this. \code NC_GRP_INFO_T* grp; ... for(i=0;ichildren);i++) { NC_GRP_INFO_T* g = nclistith(grp->children,i); ... } \endcode In this case, the iteration is by index into the underlying vector. \section Sperf Performance The initial impetus for this change was to improve the performance of netcdf-4 metadata loading by replacing linear searches with O(1) searches. In fact, this goal was not initially met. It appears to be the case that the metadata loading costs are entirely dominated by the performance of the HDF5 library. The reason for this is that the netcdf-c library used to load all the metadata immediately when a file was opened. This in turn meant that all of the metadata was immediately extracted from the underlying HDF5 file. So, there was no opportunity for lazy loading to be used. The netcdf library has since been modified to do lazy loading of variables and attributes. A not pursued alternative would have been to store a single metadata object into the file so it could be read at one shot. This object would then be processed in-memory to construct the internal metadata. The costs for this approach are space in the file plus the need to keep it consistent with the actual metadata stored by HDF5. It should be noted that there is an effect from this change. Using gprof, one can see that in the original code the obj_list_add function was the dominate function called by a large percentage (about 20%). Whereas with the new code, the function call distribution is much more even with no function taking more than 4-5%. Some other observations: 1. the utf8 code now shows up as taking about 4%. Given that most names are straight ASCII, it might pay to try to optimize for this to avoid invoking the utf8 processing code. 2. In the new code, attribute processing appears to take up a lot of the time. This, however might be an artifact of the test cases. 3. There is a small performance improvement from avoiding walking the linked list. It appears that creating a file is about 10% faster and opening a file is also about 10% faster. \section Snotes_and_warnings Notes and Warning 1. Ncxindex is currently not used for enum constants and compound fields. Additionally, it is not used for listing the dimensions associated with a variable. Small size is the reason. 2. References between meta-data objects (e.g. group parent or containing group) are stored directly and not using any kind of vector or hashtable. 3. Attribute rename and deletion are still moderately costly operations. 4. As in the original code, object ids (dimid, etc) are assigned explicitly using counters within the NC_FILE_INFO_T object. When stored into, for example, "alldims", the position of the object is forcibly made to match the value of the assigned id. 5. The file ncxindex.c has a constant, NCNOHASH, that controls if the index uses that hash table versus just searching the index's vector. This is for experimental purposes. \section Sprovenance Contact Information __Author__: Dennis Heimbigner
__Initial Version__: 01/10/2018
__Last Revised__: 10/30/2020 */