Skip to content
This repository was archived by the owner on Jun 27, 2025. It is now read-only.

Commit 946ad3a

Browse files
committed
code cleanup and notes
1 parent a6b2515 commit 946ad3a

2 files changed

Lines changed: 55 additions & 85 deletions

File tree

automap.c

Lines changed: 23 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,7 @@ static PyObject *NonUniqueError;
137137

138138

139139
// The main storage "table" is an array of TableElement
140-
typedef struct {
140+
typedef struct TableElement{
141141
Py_ssize_t keys_pos;
142142
Py_hash_t hash;
143143
} TableElement;
@@ -148,7 +148,7 @@ typedef struct {
148148
# define SCAN 16
149149

150150

151-
typedef enum {
151+
typedef enum KeysArrayType{
152152
KAT_LIST = 0, // must be falsy
153153

154154
KAT_INT8, // order matters as ranges of size are used in selection
@@ -208,7 +208,7 @@ at_to_kat(int array_t) {
208208
}
209209

210210

211-
typedef struct {
211+
typedef struct FAMObject{
212212
PyObject_VAR_HEAD
213213
Py_ssize_t table_size;
214214
TableElement *table; // an array of TableElement structs
@@ -219,7 +219,7 @@ typedef struct {
219219
} FAMObject;
220220

221221

222-
typedef enum {
222+
typedef enum ViewKind{
223223
ITEMS,
224224
KEYS,
225225
VALUES,
@@ -249,7 +249,7 @@ char_get_end_p(char* p, Py_ssize_t dt_size) {
249249

250250
static inline Py_hash_t
251251
uint_to_hash(npy_uint64 v) {
252-
Py_hash_t hash = (Py_hash_t)(v >> 1); // divide by 2 so it always fits in signed space
252+
Py_hash_t hash = (Py_hash_t)(v >> 1); // half unsigned fits in signed
253253
if (hash == -1) {
254254
return -2;
255255
}
@@ -265,6 +265,7 @@ int_to_hash(npy_int64 v) {
265265
return hash;
266266
}
267267

268+
268269
// This is a adapted from https://github.com/python/cpython/blob/ba65a065cf07a7a9f53be61057a090f7311a5ad7/Python/pyhash.c#L92
269270
#define HASH_MODULUS (((size_t)1 << 61) - 1)
270271
#define HASH_BITS 61
@@ -343,9 +344,11 @@ string_to_hash(char *str, Py_ssize_t len) {
343344

344345
static PyObject *int_cache = NULL;
345346

347+
346348
// NOTE: this used to be a Py_ssize_t, which can be 32 bits on some machines and might easily overflow with a few very large indices. Using an explicit 64-bit int seems safer
347349
static npy_int64 key_count_global = 0;
348350

351+
349352
// Fill the int_cache up to size_needed with PyObject ints; `size` is not the key_count_global.
350353
static int
351354
int_cache_fill(Py_ssize_t size_needed)
@@ -371,6 +374,7 @@ int_cache_fill(Py_ssize_t size_needed)
371374
return 0;
372375
}
373376

377+
374378
// Given the current key_count_global, remove cache elements only if the key_count is less than the the current size of the int_cache.
375379
void
376380
int_cache_remove(Py_ssize_t key_count)
@@ -388,12 +392,10 @@ int_cache_remove(Py_ssize_t key_count)
388392
//------------------------------------------------------------------------------
389393
// FrozenAutoMapIterator functions
390394

391-
typedef struct {
395+
typedef struct FAMIObject {
392396
PyObject_VAR_HEAD
393397
FAMObject *fam;
394-
PyObject **keys_fi; // fast items ptr
395398
PyArrayObject* keys_array;
396-
PyObject **int_cache_fi; // fast items ptr
397399
ViewKind kind;
398400
int reversed;
399401
Py_ssize_t index; // current index state, mutated in-place
@@ -439,14 +441,14 @@ fami_iternext(FAMIObject *self)
439441
return PyTuple_Pack(
440442
2,
441443
PyArray_ToScalar(PyArray_GETPTR1(self->keys_array, index), self->keys_array),
442-
self->int_cache_fi[index]
444+
PyList_GET_ITEM(int_cache, index)
443445
);
444446
}
445447
else {
446448
return PyTuple_Pack(
447449
2,
448-
self->keys_fi[index],
449-
self->int_cache_fi[index]
450+
PyList_GET_ITEM(self->fam->keys, index),
451+
PyList_GET_ITEM(int_cache, index)
450452
);
451453
}
452454
}
@@ -455,13 +457,13 @@ fami_iternext(FAMIObject *self)
455457
return PyArray_ToScalar(PyArray_GETPTR1(self->keys_array, index), self->keys_array);
456458
}
457459
else {
458-
PyObject* yield = self->keys_fi[index];
460+
PyObject* yield = PyList_GET_ITEM(self->fam->keys, index);
459461
Py_INCREF(yield);
460462
return yield;
461463
}
462464
}
463465
case VALUES: {
464-
PyObject *yield = self->int_cache_fi[index];
466+
PyObject *yield = PyList_GET_ITEM(int_cache, index);
465467
Py_INCREF(yield);
466468
return yield;
467469
}
@@ -515,15 +517,12 @@ fami_new(FAMObject *fam, ViewKind kind, int reversed)
515517
}
516518
Py_INCREF(fam);
517519
fami->fam = fam;
518-
if (!fam->keys_array_type) {
519-
fami->keys_fi = PySequence_Fast_ITEMS(fam->keys);
520-
fami->keys_array = NULL;
520+
if (fam->keys_array_type) {
521+
fami->keys_array = (PyArrayObject *)fam->keys;
521522
}
522523
else {
523-
fami->keys_fi = NULL;
524-
fami->keys_array = (PyArrayObject *)fam->keys;
524+
fami->keys_array = NULL;
525525
}
526-
fami->int_cache_fi = PySequence_Fast_ITEMS(int_cache);
527526
fami->kind = kind;
528527
fami->reversed = reversed;
529528
fami->index = 0;
@@ -534,7 +533,7 @@ fami_new(FAMObject *fam, ViewKind kind, int reversed)
534533
// FrozenAutoMapView functions
535534

536535
// A FAMVObject contains a reference to the FAM from which it was derived
537-
typedef struct {
536+
typedef struct FAMVObject{
538537
PyObject_VAR_HEAD
539538
FAMObject *fam;
540539
ViewKind kind;
@@ -708,8 +707,8 @@ lookup_hash(FAMObject *self, PyObject *key, Py_hash_t hash)
708707
Py_hash_t mixin = Py_ABS(hash);
709708
Py_ssize_t table_pos = hash & mask;
710709

711-
PyObject **keys_fi = PySequence_Fast_ITEMS(self->keys);
712710
PyObject *guess = NULL;
711+
PyObject *keys = self->keys;
713712
int result = -1;
714713
Py_hash_t h = 0;
715714

@@ -723,7 +722,7 @@ lookup_hash(FAMObject *self, PyObject *key, Py_hash_t hash)
723722
table_pos++;
724723
continue;
725724
}
726-
guess = keys_fi[table[table_pos].keys_pos];
725+
guess = PyList_GET_ITEM(keys, table[table_pos].keys_pos);
727726
if (guess == key) { // Hit. Object ID comparison
728727
return table_pos;
729728
}
@@ -923,6 +922,7 @@ lookup_hash_unicode(
923922
}
924923
}
925924

925+
926926
// Compare a passed char array to stored keys. This does not use any dynamic memory. Returns -1 on error.
927927
static Py_ssize_t
928928
lookup_hash_string(
@@ -2030,9 +2030,8 @@ fam_init(PyObject *self, PyObject *args, PyObject *kwargs)
20302030
}
20312031
}
20322032
else {
2033-
PyObject **keys_fi = PySequence_Fast_ITEMS(keys);
20342033
for (; i < keys_size; i++) {
2035-
if (insert(fam, keys_fi[i], i, -1)) {
2034+
if (insert(fam, PyList_GET_ITEM(keys, i), i, -1)) {
20362035
goto error;
20372036
}
20382037
}

doc/articles/npy-opt.txt

Lines changed: 32 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,55 @@
11

2-
These changes integrate direct support of NumPy arrays given as keys to `AutoMap`s and `FrozenAutoMap`s. Improvements are made in `AutoMap` initialization, whereby an array is converted to a list using specialized array methods (`tolist()`) when necessary. Improvements are made in `FrozenAutoMap` initialization, whereby an immutable array, when given as keys, is held as a reference without copy to a list; further, hashing and lookup make use of C types, avoiding any creation of PyObjects.
2+
These changes integrate direct support of NumPy arrays given as keys to `AutoMap`s and `FrozenAutoMap`s, optimizing their usage.
33

4-
Operation with non-array keys, hash table usage and scanning, and usage of the PyObject integer cache, remains unchanged.
4+
Improvements are made in `AutoMap` initialization, whereby an array is converted to a list using optimal array methods; that list is then held as the keys.
55

6-
On initialization (`fam_init`), a `KeysArrayType` enum value is assigned to the `keys_array_type` attribute of `FAMObject`. This is used for branching in all places where divergent behavior is required between keys stored as lists (as was done previously) or as keys stored as typed arrays.
6+
Improvements are made in `FrozenAutoMap` initialization, whereby an immutable array (for integer, floating point, and flexible dtypes), when given as keys, is held as a reference without copy to a list. Further, hashing and lookup make use of C types, avoiding any management of PyObjects.
77

8+
For array dtypes not explicitly handled, or for non-array keys, `FrozenAutoMap` operation is unchanged. In all cases, hash table layout and scanning, and management of the PyObject integer cache, are the same as before.
89

9-
All `FrozenAutoMap` usage of arrays reduces memory usage: no new `PyObject`s are created.
10+
A key change is that, on initialization (`fam_init`), a `KeysArrayType` enum value is assigned to the `keys_array_type` attribute of `FAMObject`. This is used for branching in all places where divergent behavior is required between keys stored as lists (as was done previously) or as keys stored as typed arrays.
1011

12+
Performance panels compare FAM(L), FAM(A), AM(A), and Dict (`FrozenAutoMap` created from a list, `FrozenAutoMap` created from an array, `AutoMap` created from an array, and a dictionary implementing an `AutoMap` mapping). Key indicators are the performance of instantiation and lookup.
1113

14+
The relevant comparison for StaticFrame usage is between FAM(A) and AM(A), the latter approximating what StaticFrame does presently when creating `FrozenAutoMap`s. (FAM(L) is not available to StaticFrame as AutoMaps are always created from an array, not a list of Python objects.)
1215

13-
General Changes
16+
Across all supported types, FAM(A) initialization is more than twice as fast as AM(A). FAM(A) lookup performance varies greatly by type, but always out-performs AM(A), in some cases more than twice as fast as AM(A). In all tests, we see signs that out-performance grows with scale.
1417

15-
More usage of `PySequence_Fast_ITEMS` where possible:
16-
In `fam_init()` for initialization from lists
17-
In `fami` structs for keys and int_cache access during iteration.
18+
Independent of performance time, All `FrozenAutoMap` usage of arrays reduces memory usage: no new `PyObject`s are created and the passed array simply has reference incremented.
19+
20+
21+
22+
Key Changes
1823

1924
Split the old `fam_new()` into `fam_new()` and `fam_init()`, implemented `__setstate__()`, `__getstate__()`:
2025
To support pickling a FAM with a NumPy array, `__setstate__()` must re-set the `writeable` flag of an arary to False.
2126
To integrate `__setstate__()`, the old `fam_new()` had to be divided into a `fam_new()` and a `fam_init()`.
2227

28+
Based on `keys_array_type`, `fam_init` calls type-specific insert routines, which use type-specific hash routines to add entries to the hash table.
29+
30+
On lookup, type-specific lookup routines are called based on `keys_array_type`. These routines identify PyObjects as PyArray scalars or native PyObject types, extract the appropriate C-type, compute a hash, and use type-specific lookup routines to discover the position in the keys array.
31+
32+
2333
Split `copy()` into `copy()` and `copy_to_new()`.
24-
Due to splitting `fam_new()`, copy allocation and copy setting needed to split into to methods.
34+
Due to now having `fam_new()` and `fam_init()`, copy allocation and copy setting needed to split into two methods. Now, in `fam_init`, if a `FAMType` is identified as the keys argument, `copy_to_new()` is used to transfer values from the argument to the new instance. The `copy()` function remains, now using `fam_new()` and `copy_to_new()`.
2535

26-
Additions to the `fam` struct:
27-
Added `key_buffer`: For Unicode arrays, this is a dynamically allocated buffer of size equal to one more than the array's max number of characters. This buffer is given to `PyUnicode_AsUCS4`, which will add a NULL and is why the size of the buffer is one more than max characters. This is only used for a FAM with Unicode array keys; all other usage keeps this as NULL.
36+
Additions to the `FAMObject` struct:
37+
`key_array_type`: A `KeysArrayType` enum specifying a list or array dtype. As a list is assigned zero (and all other array dtypes as non-zero), the value can be used to branch on non-array versus array processing.
2838

29-
Extended property tests for FAMs with arrays
30-
Tests initialization of both contiguous and non-contiguous arrays
31-
Tests all features tested for non-array-based AMs
39+
`keys_size`: As determining size must branch on `keys_array_type`, this attribute is used to track size, avoiding having to go the underly keys container.
40+
41+
`key_buffer`: For Unicode arrays, this is a dynamically allocated buffer of size equal to one more than the array's max number of characters. This buffer is given to `PyUnicode_AsUCS4`, which will add a NULL and is why the size of the buffer is one more than max characters. This is only used for a FAM with Unicode array keys; all other usage keeps this as NULL.
3242

3343
The type of `key_count_global` is now a platform independent 64 bit signed integer. Perviously, it was a `Py_ssize_t`, which is 32 bits on 32 bit systems and could overflow in scenarios when many indicies are created.
3444

45+
Extended property tests for FAMs with arrays
46+
A custom Hypothesis strategy has been implemented to deliver both contiguous and non-contiguous arrays.
47+
48+
New Hypothesis tests for array-initialized `FrozenAutoMap`s now cover all features previously tested by Hypothesis.
49+
50+
51+
52+
3553

3654

3755

@@ -123,50 +141,3 @@ Second Approach
123141

124142
Given PyObjects, can convert to C-types for type-specific loookup.
125143

126-
127-
Performance
128-
129-
NumPy unicode arrays had particularly bad performance
130-
131-
132-
Code
133-
fam_new() for normal objects
134-
insert()
135-
lookup_hash()
136-
137-
fam_new() for int arrays
138-
INSERT_SCALARS()
139-
insert_int()
140-
lookup_hash_int(): use integer as hash!
141-
142-
fam_new() for unicode arrays
143-
insert_unicode()
144-
UCS4_to_hash()
145-
lookup_hash_unicode()
146-
147-
fam_new() for AutoMap
148-
149-
150-
151-
Next Steps
152-
153-
Can we use datetime46 arrays directly?
154-
155-
156-
157-
158-
159-
160-
// if (PyArray_IsScalar(key, Byte))
161-
// npy_byte temp;
162-
// PyArray_ScalarAsCtype(key, &temp);
163-
// v = (npy_int64)temp;
164-
165-
166-
167-
else if (PyArray_IsScalar(key, Half)) {
168-
// fprintf(stderr, "got half");
169-
// double temp;
170-
// PyArray_ScalarAsCtype(key, &temp);
171-
// v = (double)temp;
172-
v = (double)PyArrayScalar_VAL(key, Half);

0 commit comments

Comments
 (0)