On 9/22/21 23:28, Ilya Maximets wrote: > On 9/21/21 12:33, Mark Gray wrote: >> On 17/09/2021 18:17, Ilya Maximets wrote: >>> Current algorithm for ovsdb_datum_subtract looks like this: >>> >>> for-each atom in a: >>> if atom in b: >>> swap(atom, <last atom in 'a'>) >>> destroy(atom) >>> quicksort(a) >>> >>> Complexity: >>> >>> Na * log2(Nb) + (Na - Nb) * log2(Na - Nb) >>> Search Comparisons for quicksort >>> >>> It's not optimal, especially because Nb << Na in a vast majority of >>> cases. >>> >>> Reversing the search phase to look up atoms from 'b' in 'a', and >>> closing gaps from deleted elements in 'a' by plain memory copy to >>> avoid quicksort. >>> >>> Resulted complexity: >>> >>> Nb * log2(Na) + (Na - Nb) >>> Search Memory copies >>> >>> Subtraction is heavily used while executing database transactions. >>> For example, to remove one port from a logical switch in OVN. >>> Complexity of such operation if original logical switch had 100 ports >>> goes down from >>> >>> 100 * log2(1) = 100 comparisons for search and >>> 99 * log2(99) = 656 comparisons for quicksort >>> ------------------------------ >>> 756 comparisons in total >>> to only >>> >>> 1 * log2(100) = 7 comparisons for search >>> + memory copy of 99 * sizeof (union ovsdb_atom) bytes. >>> >>> We could use memmove to close the gaps after removing atoms, but >>> it will lead to 2 memory copies inside the call, while we can perform >>> only one to the temporary 'result' and swap pointers. >>> >>> Performance in cases, where sizes of 'a' and 'b' are comparable, >>> should not change. Cases with Nb >> Na should not happen in practice. >> >> It seems like we are optimizing for the more common case in which Na >> >> Nb. Not sure if there are use cases in which the other case could bubble >> up as a bottleneck. Could we try to assess which case we are in by >> checking the size of the sets and then select the algorithm based on >> that assessment? i.e. have two algorithms? > > In general, case where Nb >> Na means that user wants to delete big > number of elements from a small set. This doesn't make much practical > sense, so I'm not sure if we need to optimize this scenario. > We can, sure, integrate switches for more optimal implementations > and add these more optimal implementations for particular cases if > we'll need them in the future. > >> >>> >>> All in all, this change allows ovsdb-server to perform several times >>> more transactions, that removes elements from sets, per second. >>> >>> Signed-off-by: Ilya Maximets <i.maxim...@ovn.org> >>> --- >>> lib/ovsdb-data.c | 52 +++++++++++++++++++++++++++++++++++++----------- >>> 1 file changed, 40 insertions(+), 12 deletions(-) >>> >>> diff --git a/lib/ovsdb-data.c b/lib/ovsdb-data.c >>> index 11bf95fed..b6129d6ba 100644 >>> --- a/lib/ovsdb-data.c >>> +++ b/lib/ovsdb-data.c >>> @@ -2019,26 +2019,54 @@ ovsdb_datum_subtract(struct ovsdb_datum *a, const >>> struct ovsdb_type *a_type, >>> const struct ovsdb_datum *b, >>> const struct ovsdb_type *b_type) >>> { >>> - bool changed = false; >>> - size_t i; >>> + size_t i, ai, bi, n_idx; >>> + size_t *idx; >>> >>> ovs_assert(a_type->key.type == b_type->key.type); >>> ovs_assert(a_type->value.type == b_type->value.type >>> || b_type->value.type == OVSDB_TYPE_VOID); >>> >>> - /* XXX The big-O of this could easily be improved. */ >>> - for (i = 0; i < a->n; ) { >>> - unsigned int idx = ovsdb_datum_find(a, i, b, b_type); >>> - if (idx != UINT_MAX) { >>> - changed = true; >>> - ovsdb_datum_remove_unsafe(a, i, a_type); >>> - } else { >>> - i++; >>> + idx = xmalloc(b->n * sizeof *idx); >>> + n_idx = 0; >>> + for (bi = 0; bi < b->n; bi++) { >>> + ai = ovsdb_datum_find(b, bi, a, b_type); >>> + if (ai == UINT_MAX || (n_idx && ai <= idx[n_idx - 1])) { >> >> If b and a are always sorted, will we ever hit the second clause (n_idx >> && ai <= idx[n_idx - 1])? For example, if the following elements are >> equivalent >> >> b0 -> a0 >> b1 -> a1 >> .. >> .. >> >> bi is always > bi-1 therefore ai is always greater than ai-1. If there >> is still a chance that we will hit it, could you add a more detailed >> comment? > > Yes, you're right. If both datums are correct, we don't need that > check. Will remove. > >> >>> + /* No such atom in 'a' or it's already marked for removal. */ >>> + continue; >>> } >>> + idx[n_idx++] = ai; >>> } >>> - if (changed) { >>> - ovsdb_datum_sort_assert(a, a_type->key.type); >>> + if (!n_idx) { >>> + free(idx); >>> + return; >>> } >>> + >>> + struct ovsdb_datum result; >>> + >>> + ovsdb_datum_init_empty(&result); >>> + ovsdb_datum_reallocate(&result, a_type, a->n - n_idx); >>> + >>> + for (i = 0; i < n_idx; i++) { >> >> Why don't you destroy the atoms in-place in the loop above (bi = 0; bi < >> b->n; bi++). Is it because you have to ovsdb_datum_find() each time? If >> so, maybe add a comment. > > Yes, we're not removing right away because ovsdb_datum_find() will > use them. Technically, we can change the signature of the search > function and only search starting from the previously found value, > but this unlikely to give a significant performance improvement > to justify yet another API change all over the codebase. > So I decided to just split the loop instead. I'll add a comment. > >> >>> + ai = idx[i]; >>> + >>> + /* Destroying atom. */ >>> + ovsdb_atom_destroy(&a->keys[ai], a_type->key.type); >>> + if (a_type->value.type != OVSDB_TYPE_VOID) { >>> + ovsdb_atom_destroy(&a->values[ai], a_type->value.type); >>> + } >>> + >>> + /* Copy non-removed atoms from 'a' to result. */ >>> + unsigned int start_idx = (i > 0) ? idx[i - 1] + 1 : 0; >> >> It won't save many cycles but could you initialize 'start_idx' to 0 >> outside the loop and set it to 'idx[i] + 1' after the statement below? > > I'll move the declaration of 'start_idx' out of the loop, but I > prefer keeping the condition as is. For me it looks more readable > this way. If you have value update after the usage, it's a bit > misleading, and makes you look around to check what was the previous > value.
On a second thought about this, it seems to look better this way. And I'm actually using this schema in other functions. I'll change the code to set start_idx to 'idx[i] + 1' after the copy. > >> >>> + ovsdb_datum_push_unsafe(&result, a, start_idx, ai - start_idx, >>> a_type); >>> + } >>> + /* Copying remaining atoms. */ >>> + ovsdb_datum_push_unsafe(&result, a, idx[n_idx - 1] + 1, >>> + a->n - (idx[n_idx - 1] + 1), a_type); >> >> You could also use 'start_idx' here ^ if you take my suggestion above >> which would make the code a bit easier to read IMO. > > I'll use 'start_idx' here. Good point. > >> >>> + a->n = 0; >>> + >>> + ovsdb_datum_swap(&result, a); >>> + ovsdb_datum_destroy(&result, a_type); >>> + free(idx); >>> } >>> >>> struct ovsdb_symbol_table * >>> >> >> I also did some performance testing on this and it seems to show an >> improvement! Good job! >> > > Thanks! > > Best regards, Ilya Maximets. > _______________________________________________ dev mailing list d...@openvswitch.org https://mail.openvswitch.org/mailman/listinfo/ovs-dev