trevorbaca / baca

Trevor Bača's Abjad library.
8 stars 3 forks source link

Optimize contexted indicator detach #53

Closed trevorbaca closed 2 years ago

trevorbaca commented 2 years ago

Test functions:

def make_score(note_count):
    with abjad.Timer() as timer:
        notes = [abjad.Note("c'8") for _ in range(note_count)]
        staff = abjad.Staff(notes)
        score = abjad.Score([staff])
    time = int(timer.elapsed_time)
    print(f"made {note_count} notes in {time} seconds ...")
    return score

def attach_indicators(score, indicator_classes):
    leaves = abjad.select(score).leaves() 
    total = 0
    with abjad.Timer() as timer:
        for leaf in leaves:
            for indicator_class in indicator_classes:
                indicator = indicator_class()
                abjad.attach(indicator, leaf)
                total += 1
    time = int(timer.elapsed_time)
    print(f"attached {total} contexted indicators in {time} seconds ...")

def detach_indicators(argument, indicator_classes):
    leaves = abjad.select(argument).leaves()
    total = 0
    with abjad.Timer() as timer:
        for leaf in leaves:
            for indicator_class in indicator_classes:
                result = abjad.detach(indicator_class, leaf)
                total += len(result or [])
    time = int(timer.elapsed_time)
    print(f"detached {total} contexted indicators in {time} seconds ...")

class Foo:
    context = "Staff"

class Bar:
    context = "Staff"

class Baz:
    context = "Staff"

class Qux:
    context = "Staff"

class Qul:
    context = "Staff"

Observation: runtime increases linearly with the number of contexted indicators:

print("1:")
score = make_score(200)
attach_indicators(score, [Foo])
detach_indicators(score, [Foo])
print()

print("2:")
score = make_score(200)
attach_indicators(score, [Foo, Bar])
detach_indicators(score, [Foo])
print()

print("3:")
score = make_score(200)
attach_indicators(score, [Foo, Bar, Baz])
detach_indicators(score, [Foo])
print()

print("4:")
score = make_score(200)
attach_indicators(score, [Foo, Bar, Baz, Qux])
detach_indicators(score, [Foo])
print()

print("5:")
score = make_score(200)
attach_indicators(score, [Foo, Bar, Baz, Qux, Qul])
detach_indicators(score, [Foo])
print()
1:
made 200 notes in 0 seconds ...
attached 200 contexted indicators in 0 seconds ...
detached 200 contexted indicators in 0 seconds ...

2:
made 200 notes in 0 seconds ...
attached 400 contexted indicators in 0 seconds ...
detached 200 contexted indicators in 4 seconds ...

3:
made 200 notes in 0 seconds ...
attached 600 contexted indicators in 0 seconds ...
detached 200 contexted indicators in 9 seconds ...

4:
made 200 notes in 0 seconds ...
attached 800 contexted indicators in 0 seconds ...
detached 200 contexted indicators in 14 seconds ...

5:
made 200 notes in 0 seconds ...
attached 1000 contexted indicators in 0 seconds ...
detached 200 contexted indicators in 18 seconds ...

Observation: runtime increases exponentially with the number of notes:

print("1b:")
score = make_score(300)
attach_indicators(score, [Foo])
detach_indicators(score, [Foo])
print()

print("2b:")
score = make_score(300)
attach_indicators(score, [Foo, Bar])
detach_indicators(score, [Foo])
print()

print("3b:")
score = make_score(300)
attach_indicators(score, [Foo, Bar, Baz])
detach_indicators(score, [Foo])
print()

print("4b:")
score = make_score(300)
attach_indicators(score, [Foo, Bar, Baz, Qux])
detach_indicators(score, [Foo])
print()

print("5b:")
score = make_score(300)
attach_indicators(score, [Foo, Bar, Baz, Qux, Qul])
detach_indicators(score, [Foo])
print()
1b:
made 300 notes in 0 seconds ...
attached 300 contexted indicators in 0 seconds ...
detached 300 contexted indicators in 0 seconds ...

2b:
made 300 notes in 0 seconds ...
attached 600 contexted indicators in 0 seconds ...
detached 300 contexted indicators in 10 seconds ...

3b:
made 300 notes in 0 seconds ...
attached 900 contexted indicators in 0 seconds ...
detached 300 contexted indicators in 21 seconds ...

4b:
made 300 notes in 0 seconds ...
attached 1200 contexted indicators in 0 seconds ...
detached 300 contexted indicators in 31 seconds ...

5b:
made 300 notes in 0 seconds ...
attached 1500 contexted indicators in 0 seconds ...
detached 300 contexted indicators in 44 seconds ...

Solution: remove calls to Python's inspect module from abjad.Wrapper.__eq__().

Current definition in Abjad 3.4:

    def __eq__(self, argument) -> bool:
        """
        Delegates to ``abjad.format.compare_objects()``.
        """
        return _format.compare_objects(self, argument)

Redefinition:

    def __eq__(self, argument) -> bool:
        """
        Is true when self equals ``argument``.
        """
        if not isinstance(argument, Wrapper):
            return False
        if self.annotation != argument.annotation:
            return False
        if self.component != argument.component:
            return False
        if self.context != argument.context:
            return False
        if self.deactivate != argument.deactivate:
            return False
        if self.indicator != argument.indicator:
            return False
        if self.synthetic_offset != argument.synthetic_offset:
            return False
        if self.tag != argument.tag:
            return False
        return True

Redefinition reduces runtime to zero:

print("1:")
score = make_score(200)
attach_indicators(score, [Foo])
detach_indicators(score, [Foo])
print()

print("2:")
score = make_score(200)
attach_indicators(score, [Foo, Bar])
detach_indicators(score, [Foo])
print()

print("3:")
score = make_score(200)
attach_indicators(score, [Foo, Bar, Baz])
detach_indicators(score, [Foo])
print()

print("4:")
score = make_score(200)
attach_indicators(score, [Foo, Bar, Baz, Qux])
detach_indicators(score, [Foo])
print()

print("5:")
score = make_score(200)
attach_indicators(score, [Foo, Bar, Baz, Qux, Qul])
detach_indicators(score, [Foo])
print()

print("1b:")
score = make_score(300)
attach_indicators(score, [Foo])
detach_indicators(score, [Foo])
print()

print("2b:")
score = make_score(300)
attach_indicators(score, [Foo, Bar])
detach_indicators(score, [Foo])
print()

print("3b:")
score = make_score(300)
attach_indicators(score, [Foo, Bar, Baz])
detach_indicators(score, [Foo])
print()

print("4b:")
score = make_score(300)
attach_indicators(score, [Foo, Bar, Baz, Qux])
detach_indicators(score, [Foo])
print()

print("5b:")
score = make_score(300)
attach_indicators(score, [Foo, Bar, Baz, Qux, Qul])
detach_indicators(score, [Foo])
print()
1:
made 200 notes in 0 seconds ...
attached 200 contexted indicators in 0 seconds ...
detached 200 contexted indicators in 0 seconds ...

2:
made 200 notes in 0 seconds ...
attached 400 contexted indicators in 0 seconds ...
detached 200 contexted indicators in 0 seconds ...

3:
made 200 notes in 0 seconds ...
attached 600 contexted indicators in 0 seconds ...
detached 200 contexted indicators in 0 seconds ...

4:
made 200 notes in 0 seconds ...
attached 800 contexted indicators in 0 seconds ...
detached 200 contexted indicators in 0 seconds ...

5:
made 200 notes in 0 seconds ...
attached 1000 contexted indicators in 0 seconds ...
detached 200 contexted indicators in 0 seconds ...

1b:
made 300 notes in 0 seconds ...
attached 300 contexted indicators in 0 seconds ...
detached 300 contexted indicators in 0 seconds ...

2b:
made 300 notes in 0 seconds ...
attached 600 contexted indicators in 0 seconds ...
detached 300 contexted indicators in 0 seconds ...

3b:
made 300 notes in 0 seconds ...
attached 900 contexted indicators in 0 seconds ...
detached 300 contexted indicators in 0 seconds ...

4b:
made 300 notes in 0 seconds ...
attached 1200 contexted indicators in 0 seconds ...
detached 300 contexted indicators in 0 seconds ...

5b:
made 300 notes in 0 seconds ...
attached 1500 contexted indicators in 0 seconds ...
detached 300 contexted indicators in 0 seconds ...

abjad.format.compare_objects() is used only in definitions of __eq__().

Conclusion: remove abjad.format.compare_objects().

trevorbaca commented 2 years ago

Opened as Abjad #1379.