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?

> 
> 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?

> +            /* 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.

> +        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?

> +        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.

> +    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!

_______________________________________________
dev mailing list
d...@openvswitch.org
https://mail.openvswitch.org/mailman/listinfo/ovs-dev

Reply via email to