ЁЯПл The SchoolтА║ЁЯПОя╕П PerformanceтА║ЁЯУИ рдзрдбрд╛ 04 тАФ рдкреНрд░рддреНрдпрдХреНрд╖рд╛рддрд▓реА complexity: рдЬреЛрдбреА-рдЬреЛрдбреАрдиреЗ, рдХреА рдЖрдзреА sort?
ЁЯЦ╝я╕П See the drawing + lab ЁЯПа Course home ЁЯМ┐ Branch on GitHub тЬПя╕П View source
ЁЯЦ╝я╕П рдЖрдХреГрддреА рдЖрдгрд┐ labThe drawing + lab рдкреВрд░реНрдг рдкрд╛рдирд╛рд╡рд░ рдЙрдШрдбрд╛ тЖЧOpen full page тЖЧ

ЁЯУИ рдзрдбрд╛ 04 тАФ рдкреНрд░рддреНрдпрдХреНрд╖рд╛рддрд▓реА complexity: рдЬреЛрдбреА-рдЬреЛрдбреАрдиреЗ, рдХреА рдЖрдзреА sort?

ЁЯУН рддреБрдореНрд╣реА рдЗрдереЗ рдЖрд╣рд╛рдд: 12 рдкреИрдХреА рдзрдбрд╛ 04 ┬╖ рдорд╛рдЧреЗ: lesson-03-profiling ┬╖ рдкреБрдвреЗ: lesson-05-memory


ЁЯУж рдпрд╛ рдмреНрд░рдБрдЪрдордзреНрдпреЗ рдХрд╛рдп рдЖрд╣реЗ

рдзрдбреЗ 01тАУ03, рдЕрдзрд┐рдХ algorithmic complexity, рдкрд╛рда рдХрд░реВрди рдирд╡реНрд╣реЗ рддрд░ рдореЛрдЬреВрди: рдПрдХрдЪ рдкреНрд░рд╢реНрди тАФ "рдХреЛрдгрддреЗ bib рдХреНрд░рдорд╛рдВрдХ рджреЛрдирджрд╛ рдЖрд▓реЗ рдЖрд╣реЗрдд рдХрд╛?" тАФ рддреАрди рдкреНрд░рдХрд╛рд░реЗ рд╕реЛрдбрд╡рд▓реЗрд▓рд╛, рдкреНрд░рддреНрдпреЗрдХ comparison рдореЛрдЬреВрди: рдкреНрд░рддреНрдпреЗрдХ рдЬреЛрдбреА (O(n┬▓)), рдЖрдзреА sort рдХрд░реВрди рдордЧ рд╢реЗрдЬрд╛рд░реА рддрдкрд╛рд╕рдгреЗ (O(n log n)), рдЖрдгрд┐ set (рд╕рд░рд╛рд╕рд░реА O(n)). perf/demo.py рдордзрд▓реЗ complexity() рдЖрдгрд┐ perf/sim.py рдордзрд▓реЗ dup_pairs, dup_sorted, merge_sort рдЖрдгрд┐ dup_set.

ЁЯзТ 5 рд╡рд░реНрд╖рд╛рдВрдЪреНрдпрд╛ рдореБрд▓рд╛рд▓рд╛ рд╕рдордЬрд╛рд╡рд▓реНрдпрд╛рд╕рд╛рд░рдЦреЗ

рд╢рд░реНрдпрддреАрдЖрдзреА рдкреНрд░рддреНрдпреЗрдХ рдзрд╛рд╡рдкрдЯреВрд▓рд╛ рдПрдХ bib рдХреНрд░рдорд╛рдВрдХ рдорд┐рд│рддреЛ. ЁЯФв рджреЛрди рдзрд╛рд╡рдкрдЯреВрдВрдЪрд╛ рдХреНрд░рдорд╛рдВрдХ рдХрдзреАрдЪ рд╕рд╛рд░рдЦрд╛ рдЕрд╕рддрд╛ рдХрд╛рдорд╛ рдирдпреЗ. рдРрд╢реНрд╡рд░реНрдпрд╛рд▓рд╛ рд╣реЗ рддрдкрд╛рд╕рд╛рдпрдЪреЗ рдЖрд╣реЗ.

рдкреНрд░рддреНрдпреЗрдХ рдкрджреНрдзрддреАрдЪреЗ рдЙрддреНрддрд░ рдПрдХрдЪ. рдХрд╛рдорд╛рдЪреЗ рдкреНрд░рдорд╛рдг рдорд╛рддреНрд░ рдЦреВрдк рд╡реЗрдЧрд╡реЗрдЧрд│реЗ.

ЁЯЧ║я╕П рдЖрдХреГрддреА

flowchart LR
    q["тЭУ any duplicate bib<br/>among n runners?"] --> a["every pair ┬╖ O(n┬▓)<br/>n = 2000 тЖТ 1,999,000"]
    q --> b["sort + neighbours ┬╖ O(n log n)<br/>n = 2000 тЖТ 21,436"]
    q --> c["a set ┬╖ O(n)<br/>n = 2000 тЖТ 2,000"]
    a --> g["double n тЖТ 4├Ч the work"]
    b --> h["double n тЖТ a bit over 2├Ч"]
    c --> i["double n тЖТ 2├Ч"]

ЁЯЧ║я╕П рдХрд╛рдврд▓реЗрд▓реА рдЖрдХреГрддреА + рдПрдХ lab: https://school-edh.pages.dev/performance/lesson-diagrams.html#l04

тЭУ рдХрд╛рдп

рдпрд╛рдорд╛рдЧрдЪреА data structures (sorting, hashing, trees) рд╣рд╛ DSA school рдЪрд╛ рд╡рд┐рд╖рдп рдЖрд╣реЗ.

ЁЯдФ рдХрд╛

рдХрд╛рд░рдг рдХрд┐рддреАрд╣реА tuning рдХреЗрд▓реЗ рддрд░реА рдЪреБрдХреАрдЪрд╛ algorithm рд╡рд╛рдЪрдд рдирд╛рд╣реА. рдЬреЛрдбреА-рдЬреЛрдбреАрдЪрд╛ loop рджреБрдкреНрдкрдЯ рдЬрд▓рдж рдХреЗрд▓реНрдпрд╛рдиреЗ рд╢рд╛рд│реЗрдЪрд╛ рдЖрдХрд╛рд░ рдлрдХреНрдд рдПрдХрджрд╛ рджреБрдкреНрдкрдЯ рд╣реЛрдИрдкрд░реНрдпрдВрдд рд╡реЗрд│ рдорд┐рд│рддреЛ; рддреНрдпрд╛рдирдВрддрд░ рддреЛ рдкреБрдиреНрд╣рд╛ рд╣рд│реВ рд╣реЛрддреЛ. O(n┬▓) рдмрджрд▓реВрди O(n log n) рдХреЗрд▓реНрдпрд╛рдиреЗ рд╕рдВрдкреВрд░реНрдг рд╡рдХреНрд░рд░реЗрд╖рд╛рдЪ рдмрджрд▓рддреЗ. рдЖрдгрд┐ complexity bugs tests рдордзреНрдпреЗ рд▓рдкрддрд╛рдд: 20 rows рд╡рд░ рдкреНрд░рддреНрдпреЗрдХ рдкрджреНрдзрдд рдХреНрд╖рдгрд╛рдд рд╣реЛрддреЗ. рддреЗ production рдордзреНрдпреЗ рджрд┐рд╕рддрд╛рдд, рдЬреНрдпрд╛ рджрд┐рд╡рд╢реА data рдореЛрдард╛ рд╣реЛрддреЛ.

ЁЯФз рдХрд╕реЗ (рдпрд╛ repo рдордзреНрдпреЗ)

perf/sim.py рдордзрд▓реЗ bibs(n) seeded random рдХреНрд░рдорд╛рдиреЗ n рд╡реЗрдЧрд╡реЗрдЧрд│реЗ рдХреНрд░рдорд╛рдВрдХ рджреЗрддреЗ. dup_pairs рдкреНрд░рддреНрдпреЗрдХ рдЬреЛрдбреАрдЪреА рддреБрд▓рдирд╛ рдХрд░рддреЗ; dup_sorted merge_sort(xs, count) call рдХрд░рддреЗ тАФ рдПрдХ рд╕рд╛рдзрд╛ merge sort рдЬреЛ рдкреНрд░рддреНрдпреЗрдХ comparison рд╕рд╛рдареА count[0] рдордзреНрдпреЗ 1 рдЬреЛрдбрддреЛ тАФ рдордЧ рд╢реЗрдЬрд╛рд░реА рддрдкрд╛рд╕рддреЗ; dup_set рдкреНрд░рддреНрдпреЗрдХ item рд╕рд╛рдареА рдПрдХ set lookup рдХрд░рддреЗ. рдкреНрд░рддреНрдпреЗрдХ (found a duplicate?, comparisons) рдкрд░рдд рджреЗрддреЗ. perf/demo.py рдордзрд▓реЗ complexity() n рд▓рд╛ 250 рдкрд╛рд╕реВрди 2000 рдкрд░реНрдпрдВрдд рджреБрдкреНрдкрдЯ рдХрд░рдд рдЬрд╛рддреЗ.

ЁЯзк рдХрд░реВрди рдкрд╛рд╣рд╛

python3 perf/demo.py complexity
python3 - <<'EOF'
import math, sys; sys.path.insert(0, "perf"); from sim import bibs, dup_pairs, dup_sorted
for n in (1000, 2000, 4000):
    b = bibs(n); srt = dup_sorted(b)[1]
    print(f"n {n:>5}: every pair n(n-1)/2 = {n * (n - 1) // 2:>9,} ┬╖ merge sort + scan {srt:>6,} ┬╖ n┬╖log2(n) = {round(n * math.log2(n)):>6,}")
b = bibs(1000); b[700] = b[3]
print("with one duplicate: every pair", dup_pairs(b), "┬╖ sorted", dup_sorted(b))
EOF

тЬЕ рддрдкрд╛рд╕рд╛ тАФ рддреБрдореНрд╣рд╛рд▓рд╛ рдХрд╛рдп рджрд┐рд╕рд╛рдпрд▓рд╛ рд╣рд╡реЗ

complexity рд╣реЗ рдЫрд╛рдкрддреЗ:

       n   every pair O(n┬▓)   sort + neighbours O(n log n)   a set O(n)
     250             31,125                          1,921          250
     500            124,750                          4,355          500
    1000            499,500                          9,724        1,000
    2000          1,999,000                         21,436        2,000
   double n: every pair does 4├Ч the work ┬╖ sorting a little over 2├Ч ┬╖ the set exactly 2├Ч
   big-O is how the work GROWS; at n = 2000 the pairwise way already does 93├Ч the comparisons of sorting

рддреБрдордЪрд╛ snippet рд╣реЗ рдЫрд╛рдкрддреЛ:

n  1000: every pair n(n-1)/2 =   499,500 ┬╖ merge sort + scan  9,724 ┬╖ n┬╖log2(n) =  9,966
n  2000: every pair n(n-1)/2 = 1,999,000 ┬╖ merge sort + scan 21,436 ┬╖ n┬╖log2(n) = 21,932
n  4000: every pair n(n-1)/2 = 7,998,000 ┬╖ merge sort + scan 46,873 ┬╖ n┬╖log2(n) = 47,863
with one duplicate: every pair (True, 3691) ┬╖ sorted (True, 9439)

ЁЯПБ рддреБрдореНрд╣реА рдЖрддреНрддрд╛рдЪ рдХрд╛рдп рд╕рд┐рджреНрдз рдХреЗрд▓реЗ

Counts рд╕реВрддреНрд░рд╛рдВрдиреБрд╕рд╛рд░рдЪ рдпреЗрддрд╛рдд: рдкреНрд░рддреНрдпреЗрдХ рдЬреЛрдбреА рдореНрд╣рдгрдЬреЗ рдиреЗрдордХреЗ n(nтИТ1)/2, рдЖрдгрд┐ merge sort n┬╖logтВВn рдЪреНрдпрд╛ рдХрд┐рдВрдЪрд┐рдд рдЦрд╛рд▓реА рд░рд╛рд╣рддреЛ. 250 рддреЗ 2000 рдзрд╛рд╡рдкрдЯреВ (8├Ч рдЬрд╛рд╕реНрдд), рдЬреЛрдбреА-рдЬреЛрдбреАрдЪреНрдпрд╛ рдкрджреНрдзрддреАрдиреЗ 64├Ч рдЬрд╛рд╕реНрдд рдХрд╛рдо рдХреЗрд▓реЗ, sort рдиреЗ рд╕реБрдорд╛рд░реЗ 11├Ч, set рдиреЗ 8├Ч. рдореБрджреНрджрд╛рдо рдареЗрд╡рд▓реЗрд▓реНрдпрд╛ рджреБрд╣реЗрд░реА рдХреНрд░рдорд╛рдВрдХрд╛рд╕рд╣, рдЬреЛрдбреА-рдЬреЛрдбреАрдЪреА рдкрджреНрдзрдд 3,691 comparisons рдирдВрддрд░ рдерд╛рдВрдмрд▓реА тАФ рдЗрдереЗ рдирд╢реАрдм рдЪрд╛рдВрдЧрд▓реЗ рд╣реЛрддреЗ, рдХрд╛рд░рдг рджреБрд╣реЗрд░реА рдХреНрд░рдорд╛рдВрдХ рд╕реБрд░реБрд╡рд╛рддреАрдЬрд╡рд│ рд╣реЛрддрд╛; рддреБрдореНрд╣реА рдирд╢рд┐рдмрд╛рд╡рд░ рдЕрд╡рд▓рдВрдмреВрди рд░рд╛рд╣реВ рд╢рдХрдд рдирд╛рд╣реА.

тЪая╕П рдиреЗрд╣рдореАрдЪреНрдпрд╛ рдЪреБрдХрд╛

ЁЯПн рдкреНрд░рддреНрдпрдХреНрд╖ рд╡рд╛рдкрд░рд╛рдд

рджреБрдкрдЯреАрдЪреНрдпрд╛ test рдиреЗ рдХрд╛рдо рдХрд╕реЗ рд╡рд╛рдврддреЗ рддреЗ рддрдкрд╛рд╕рд╛: рдХрд╛рдо n, 2n рдЖрдгрд┐ 4n рд╡рд░ рдореЛрдЬрд╛ (рд╡реЗрд│ рдХрд┐рдВрд╡рд╛ count). рдЬрд░ рдкреНрд░рддреНрдпреЗрдХ рджреБрдкрдЯреАрд▓рд╛ рд╕реБрдорд╛рд░реЗ 4├Ч рдЬрд╛рд╕реНрдд рд╡реЗрд│ рд▓рд╛рдЧрдд рдЕрд╕реЗрд▓, рддрд░ рдХреБрдареЗрддрд░реА O(n┬▓) рдЖрд╣реЗ. рдЦрд▒реНрдпрд╛ machine рд╡рд░:

for n in 1000 2000 4000; do
  python3 -c "import time; xs=list(range($n)); t=time.perf_counter(); [x for x in xs if x in xs]; print($n, round(time.perf_counter()-t, 3), 's')"
done      # x in a list, inside a loop: each doubling тЙИ 4├Ч the time

рдХрд╛рдо рдХрдореА рдареЗрд╡рдгрд╛рд░реА standard-library tools:

import bisect, heapq
from collections import Counter
dupes = [b for b, c in Counter(bibs).items() if c > 1]   # O(n) with a hash map
top10 = heapq.nsmallest(10, laps)                        # O(n log 10), no full sort
i = bisect.bisect_left(sorted_bibs, 1234)               # O(log n) lookup in sorted data

ЁЯПн Production рдордзреНрдпреЗ рд╣реЗ рдХрд╛ рдорд╣рддреНрддреНрд╡рд╛рдЪреЗ рдЖрд╣реЗ: рдЬреЗрд╡реНрд╣рд╛ рдПрдЦрд╛рджреНрдпрд╛ job рдЪрд╛ рд╡реЗрд│ рддреНрдпрд╛рдЪреНрдпрд╛ data рдкреЗрдХреНрд╖рд╛ рдЬрд╛рд╕реНрдд рд╡реЗрдЧрд╛рдиреЗ рд╡рд╛рдврддреЛ, рддреЗрд╡реНрд╣рд╛ loop рдЪреНрдпрд╛ рдЖрддрд▓рд╛ loop рд╢реЛрдзрд╛ тАФ рдмрд▒реНрдпрд╛рдЪрджрд╛ list рд╡рд░рдЪрд╛ in, рдкреНрд░рддреНрдпреЗрдХ row рд╕рд╛рдареА рдПрдХ query, рдХрд┐рдВрд╡рд╛ рдкреНрд░рддреНрдпреЗрдХ item рд╕рд╛рдареА рдПрдХ sort.

тПня╕П рдкреБрдвреЗ

рд╡реЗрд│ рд╣рд╛ рдПрдХ рдЦрд░реНрдЪ рдЖрд╣реЗ. Memory рд╣рд╛ рджреБрд╕рд░рд╛: рд╕рдЧрд│реЗ рдПрдХрджрдо рдзрд░реВрди рдареЗрд╡рдгреЗ, рдХреА рдПрдХрд╛ рд╡реЗрд│реА рдПрдХ item. рдкреБрдвреЗ: lists, generators рдЖрдгрд┐ tracemalloc.

git checkout lesson-05-memory

ЁЯУИ Lesson 04 тАФ Complexity in practice: pair by pair, or sort first?

ЁЯУН You are here: Lesson 04 of 12 ┬╖ Previous: lesson-03-profiling ┬╖ Next: lesson-05-memory


ЁЯУж What's in this branch

Lessons 01тАУ03, plus algorithmic complexity, measured instead of recited: the same question тАФ "are there any duplicate bib numbers?" тАФ answered three ways, with every comparison counted: every pair (O(n┬▓)), sort then check neighbours (O(n log n)), and a set (O(n) on average). complexity() in perf/demo.py and dup_pairs, dup_sorted, merge_sort and dup_set in perf/sim.py.

ЁЯзТ Explain like I'm 5

Before the race, every runner gets a bib number. ЁЯФв Two runners must never have the same one. Aishwarya has to check.

Same answer every way. Very different amounts of work.

ЁЯЧ║я╕П Diagram

flowchart LR
    q["тЭУ any duplicate bib<br/>among n runners?"] --> a["every pair ┬╖ O(n┬▓)<br/>n = 2000 тЖТ 1,999,000"]
    q --> b["sort + neighbours ┬╖ O(n log n)<br/>n = 2000 тЖТ 21,436"]
    q --> c["a set ┬╖ O(n)<br/>n = 2000 тЖТ 2,000"]
    a --> g["double n тЖТ 4├Ч the work"]
    b --> h["double n тЖТ a bit over 2├Ч"]
    c --> i["double n тЖТ 2├Ч"]

ЁЯЧ║я╕П Drawn version + a lab: https://school-edh.pages.dev/performance/lesson-diagrams.html#l04

тЭУ What

The data structures behind these (sorting, hashing, trees) are the DSA school.

ЁЯдФ Why

Because no amount of tuning saves the wrong algorithm. Making the pairwise loop twice as fast buys you one doubling of the school size; after that, it is slow again. Changing O(n┬▓) to O(n log n) changes the whole curve. And complexity bugs hide in tests: on 20 rows every method is instant. They appear in production, on the day the data gets big.

ЁЯФз How (in this repo)

bibs(n) in perf/sim.py gives n different numbers in a seeded random order. dup_pairs compares every pair; dup_sorted calls merge_sort(xs, count) тАФ a plain merge sort that adds 1 to count[0] per comparison тАФ then checks neighbours; dup_set does one set lookup per item. Each returns (found a duplicate?, comparisons). complexity() in perf/demo.py doubles n from 250 to 2000.

ЁЯзк Try it

python3 perf/demo.py complexity
python3 - <<'EOF'
import math, sys; sys.path.insert(0, "perf"); from sim import bibs, dup_pairs, dup_sorted
for n in (1000, 2000, 4000):
    b = bibs(n); srt = dup_sorted(b)[1]
    print(f"n {n:>5}: every pair n(n-1)/2 = {n * (n - 1) // 2:>9,} ┬╖ merge sort + scan {srt:>6,} ┬╖ n┬╖log2(n) = {round(n * math.log2(n)):>6,}")
b = bibs(1000); b[700] = b[3]
print("with one duplicate: every pair", dup_pairs(b), "┬╖ sorted", dup_sorted(b))
EOF

тЬЕ Verify тАФ what you should see

complexity prints:

       n   every pair O(n┬▓)   sort + neighbours O(n log n)   a set O(n)
     250             31,125                          1,921          250
     500            124,750                          4,355          500
    1000            499,500                          9,724        1,000
    2000          1,999,000                         21,436        2,000
   double n: every pair does 4├Ч the work ┬╖ sorting a little over 2├Ч ┬╖ the set exactly 2├Ч
   big-O is how the work GROWS; at n = 2000 the pairwise way already does 93├Ч the comparisons of sorting

Your snippet prints:

n  1000: every pair n(n-1)/2 =   499,500 ┬╖ merge sort + scan  9,724 ┬╖ n┬╖log2(n) =  9,966
n  2000: every pair n(n-1)/2 = 1,999,000 ┬╖ merge sort + scan 21,436 ┬╖ n┬╖log2(n) = 21,932
n  4000: every pair n(n-1)/2 = 7,998,000 ┬╖ merge sort + scan 46,873 ┬╖ n┬╖log2(n) = 47,863
with one duplicate: every pair (True, 3691) ┬╖ sorted (True, 9439)

ЁЯПБ What you just proved

The counts follow the formulas: every pair is exactly n(nтИТ1)/2, and the merge sort stays just under n┬╖logтВВn. From 250 to 2000 runners (8├Ч more), the pairwise way did 64├Ч more work, the sort about 11├Ч, the set 8├Ч. With a planted duplicate, the pairwise way stopped after 3,691 comparisons тАФ lucky here, because the duplicate was near the front; you cannot count on luck.

тЪая╕П Common mistakes

ЁЯПн In production

Check how work grows with a doubling test: time (or count) the job at n, 2n and 4n. If each doubling takes about 4├Ч longer, you have an O(n┬▓) somewhere. On a real machine:

for n in 1000 2000 4000; do
  python3 -c "import time; xs=list(range($n)); t=time.perf_counter(); [x for x in xs if x in xs]; print($n, round(time.perf_counter()-t, 3), 's')"
done      # x in a list, inside a loop: each doubling тЙИ 4├Ч the time

Standard-library tools that keep work small:

import bisect, heapq
from collections import Counter
dupes = [b for b, c in Counter(bibs).items() if c > 1]   # O(n) with a hash map
top10 = heapq.nsmallest(10, laps)                        # O(n log 10), no full sort
i = bisect.bisect_left(sorted_bibs, 1234)               # O(log n) lookup in sorted data

ЁЯПн Why this matters in production: when a job's time grows faster than its data, look for a loop inside a loop тАФ often an in on a list, a query per row, or a sort per item.

тПня╕П Next

Time is one cost. Memory is the other: holding everything at once, or one item at a time. Next: lists, generators and tracemalloc.

git checkout lesson-05-memory
тЖР PreviousprofilingNext тЖТmemory

This page is the lesson's README from the lesson-04-complexity branch, shown here so the whole School stays on one site. Code files open on GitHub at the same branch.