Skip to content

Commit

Permalink
Add a bulk method in C for file load speedups.
Browse files Browse the repository at this point in the history
Read times are again better than before:

read  average 6.240402834334721 stddev 0.5385303523379349
write average 7.7160701316703735 stddev 0.505427296067659
  • Loading branch information
jamadden committed Sep 20, 2016
1 parent 8107f3f commit d8cf05f
Show file tree
Hide file tree
Showing 5 changed files with 114 additions and 37 deletions.
49 changes: 41 additions & 8 deletions relstorage/cache/lru.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,26 +33,32 @@
_eden_add = _FFI_RING.eden_add
_lru_on_hit = _FFI_RING.lru_on_hit
_lru_age_lists = _FFI_RING.lru_age_lists
_eden_add_many = _FFI_RING.eden_add_many

class SizedLRURingEntry(object):

__slots__ = ('key', 'value',
'cffi_ring_node', 'cffi_ring_handle')
__slots__ = (
'key', 'value',
'cffi_ring_node', 'cffi_ring_handle',
# This is an owning pointer that is allocated when we
# are imported from a persistent file. It keeps a whole array alive
'_cffi_owning_node'
)

def __init__(self, key, value):
def __init__(self, key, value, node=None):
self.key = key
self.value = value

# Passing the string is faster than passing a cdecl because we
# have the string directly in bytecode without a lookup
node = ffi_new('CPersistentRing*')
if node is None:
node = ffi_new('CPersistentRing*')
self.cffi_ring_node = node

# Directly setting attributes is faster than the initializer
handle = self.cffi_ring_handle = ffi_new_handle(self)
node.user_data = handle
node.len = len(key) + len(value)
node.user_data = self.cffi_ring_handle = ffi_new_handle(self)
node.frequency = 1
node.len = len(key) + len(value)

def reset(self, key, value):
self.key = key
Expand Down Expand Up @@ -155,14 +161,41 @@ def __init__(self, limit, probation_lru, protected_lru, entry_dict):
self.entry_dict = entry_dict
self._node_free_list = []

def _preallocate_entries(self, ordered_keys_and_values):
count = len(ordered_keys_and_values)
nodes = ffi.new('CPersistentRing[]', count)
entries = []
for i in range(count):
k, v = ordered_keys_and_values[i]
node = nodes + i # this gets CPersistentRing*; nodes[i] returns the struct
entry = SizedLRURingEntry(k, v, node)
entry._cffi_owning_node = nodes
entries.append(entry)
return nodes, entries

def add_MRUs(self, ordered_keys_and_values):


nodes, entries = self._preallocate_entries(ordered_keys_and_values)
number_nodes = len(ordered_keys_and_values)
added_count = _eden_add_many(self._ring_home,
self._protected_lru_ring_home,
self._probation_lru_ring_home,
nodes,
number_nodes)
# Only return the objects we added, allowing the rest to become garbage.
if added_count < number_nodes:
return entries[:added_count]
return entries

def add_MRU(self, key, value):
node_free_list = self._node_free_list
if node_free_list:
new_entry = node_free_list.pop()
new_entry.reset(key, value)
else:
new_entry = SizedLRURingEntry(key, value)
rejected_items = _eden_add(self._ring.ring_home,
rejected_items = _eden_add(self._ring_home,
self._protected_lru_ring_home,
self._probation_lru_ring_home,
new_entry.cffi_ring_node)
Expand Down
28 changes: 8 additions & 20 deletions relstorage/cache/mapping.py
Original file line number Diff line number Diff line change
Expand Up @@ -260,28 +260,15 @@ def load_from_file(self, cache_file):

def _insert_entries(entries):
stored = 0

# local optimizations
data = self._dict
main = self._eden
ring_add = main.add_MRU
limit = self.limit
# Cache and track the size locally so we're not
# reading from CFFI every time.
size = self.size

for k, v in entries:
if k in data:
continue

entry = data[k] = ring_add(k, v)
size += entry.len
added_entries = self._eden.add_MRUs(entries)

for e in added_entries:
assert e.key not in data
data[e.key] = e
stored += 1

if size >= limit:
break

return stored

stored = 0
Expand All @@ -293,9 +280,10 @@ def _insert_entries(entries):
# Loading more data into an existing bucket.
# Load only the *new* keys, trying to get the newest ones
# because LRU is going to get messed up anyway.

entries_newest_first = reversed(entries_oldest_first)
stored = _insert_entries(entries_newest_first)
new_entries_newest_first = [t for t in entries_oldest_first
if t[0] not in self._dict]
new_entries_newest_first.reverse()
stored = _insert_entries(new_entries_newest_first)

then = time.time()
log.info("Examined %d and stored %d items from %s in %s",
Expand Down
8 changes: 4 additions & 4 deletions relstorage/cache/micro_benchmark_results.rst
Original file line number Diff line number Diff line change
Expand Up @@ -200,8 +200,8 @@ write average 7.996576041332446 stddev 0.05586695632417015

When we stop aging an write and limit simply by byte count, and start
flowing items through eden, not just the protected ring, our write
time goes back to about 2.6s, while (with some other optimizations)
our read time decreases to 1.1s, giving these benchmarks:
time goes back to about 2.6s. Our read time increased substantially,
so we added a bulk method in C, giving us times once again comparable:

read average 3.9413027376673804 stddev 0.24814264079261292
write average 8.434393537667347 stddev 0.155851543062577
read average 6.240402834334721 stddev 0.5385303523379349
write average 7.7160701316703735 stddev 0.505427296067659
60 changes: 55 additions & 5 deletions relstorage/cache/ring.c
Original file line number Diff line number Diff line change
Expand Up @@ -160,10 +160,18 @@ static int lru_will_fit(CPersistentRing* ring, CPersistentRing* entry)
return ring->max_len >= (entry->len + ring->frequency);
}

CPersistentRing eden_add(CPersistentRing* eden_ring,
CPersistentRing* protected_ring,
CPersistentRing* probation_ring,
CPersistentRing* entry)
/**
* When `allow_victims` is Falce, then we stop once we fill up all
* three rings and we avoid producing any victims. If we *would*
* have produced victims, we return with rejects.frequency = 1 so the
* caller can know to stop feeding us.
*/
static
CPersistentRing _eden_add(CPersistentRing* eden_ring,
CPersistentRing* protected_ring,
CPersistentRing* probation_ring,
CPersistentRing* entry,
int allow_victims)
{
CPersistentRing rejects = {};
rejects.r_next = rejects.r_prev = NULL;
Expand All @@ -175,7 +183,7 @@ CPersistentRing eden_add(CPersistentRing* eden_ring,

// Ok, we have to move things. Begin by filling up the
// protected space
if( ring_is_empty(probation_ring) && !ring_oversize(protected_ring)) {
if(ring_is_empty(probation_ring) && !ring_oversize(protected_ring)) {
/*
# This is a modification of the algorithm. When we start out
# go ahead and populate the protected_lru directly
Expand All @@ -196,6 +204,8 @@ CPersistentRing eden_add(CPersistentRing* eden_ring,
# This may temporarily oversize us in the aggregate of the three.
*/
ring_move_to_head_from_foreign(eden_ring, probation_ring, eden_oldest);
// Signal whether we would need to cull something.
rejects.frequency = ring_oversize(probation_ring);
break;
}
else {
Expand All @@ -222,6 +232,13 @@ CPersistentRing eden_add(CPersistentRing* eden_ring,
else {
// Darn, we're too big. We must choose (and record) a
// victim.

if(!allow_victims) {
// set the signal and quit.
rejects.frequency = 1;
break;
}

CPersistentRing* probation_oldest = ring_lru(probation_ring);
if(!probation_oldest) {
//Hmm, the ring got emptied, but there's also no space
Expand Down Expand Up @@ -253,6 +270,39 @@ CPersistentRing eden_add(CPersistentRing* eden_ring,
return rejects;
}

CPersistentRing eden_add(CPersistentRing* eden_ring,
CPersistentRing* protected_ring,
CPersistentRing* probation_ring,
CPersistentRing* entry)
{
return _eden_add(eden_ring, protected_ring, probation_ring, entry, 1);
}


int eden_add_many(CPersistentRing* eden_ring,
CPersistentRing* protected_ring,
CPersistentRing* probation_ring,
CPersistentRing* entry_array,
int entry_count)
{
int i = 0;
for (i = 0; i < entry_count; i++) {
CPersistentRing add_rejects = _eden_add(eden_ring, protected_ring, probation_ring, entry_array + i, 0);
if (add_rejects.frequency) {
// We would have rejected something, so we must be full.
// XXX: This isn't strictly true. It could be one really
// large item in the middle that we can't fit, but we
// might be able to fit items after it.
break;
}
}

return i;
}




static void lru_age_list(CPersistentRing* ring)
{
if (ring_is_empty(ring)) {
Expand Down
6 changes: 6 additions & 0 deletions relstorage/cache/ring.h
Original file line number Diff line number Diff line change
Expand Up @@ -92,3 +92,9 @@ CPersistentRing eden_add(CPersistentRing* eden_ring,
void lru_on_hit(CPersistentRing* ring, CPersistentRing* entry);

void lru_age_lists(CPersistentRing* eden_ring, CPersistentRing* protected_ring, CPersistentRing* probation_ring);

int eden_add_many(CPersistentRing* eden_ring,
CPersistentRing* protected_ring,
CPersistentRing* probation_ring,
CPersistentRing* entry_array,
int entry_count);

0 comments on commit d8cf05f

Please sign in to comment.