compile-once-call-many
When to Use
- Same expression evaluated millions of times
- eval() or regex in a loop
- Formula/pattern is fixed, only values change
- Profiling shows string parsing as bottleneck
When NOT to Use
- Expression changes each iteration
- Only called a few times
- Code clarity more important than speed
The Pattern
Transform string formula to compiled function, then call the function.
# SLOW: eval in loop
for values in million_combinations:
if eval(f"{values[0]} + {values[1]} == {values[2]}"):
results.append(values)
# FAST: compile once, call many
formula = "lambda a, b, c: a + b == c"
check = eval(formula)
for values in million_combinations:
if check(*values):
results.append(values)
Example (from pytudes Cryptarithmetic.ipynb)
def solve(formula):
"""Slow version: eval in loop."""
for digits in permutations('1234567890', len(letters)):
filled = substitute(digits, letters, formula)
if eval(filled): # eval called 3.6 million times!
yield filled
def faster_solve(formula):
"""Fast version: compile once, call many."""
# Transform "NUM + BER = PLAY" to lambda
fn_str, letters = translate_formula(formula)
# fn_str = "lambda A,B,E,L,M,N,P,R,U,Y: (100*N+10*U+M) + (100*B+10*E+R) == ..."
formula_fn = eval(fn_str) # Compile once
for digits in permutations((1,2,3,4,5,6,7,8,9,0), len(letters)):
try:
if formula_fn(*digits): # Call compiled function
yield format_solution(digits, letters, formula)
except ArithmeticError:
pass
def translate_formula(formula):
"""Turn 'NUM + BER = PLAY' into evaluatable lambda."""
letters = sorted(set(re.findall('[A-Z]', formula)))
# Convert words to arithmetic: NUM -> (100*N + 10*U + M)
def word_to_expr(match):
word = match.group()
terms = [f"{10**(len(word)-i-1)}*{c}" for i, c in enumerate(word)]
return f"({' + '.join(terms)})"
body = re.sub('[A-Z]+', word_to_expr, formula.replace('=', '=='))
return f"lambda {','.join(letters)}: {body}", letters
# Result: 15x speedup!
Key Principles
- Profile first: Confirm the bottleneck is evaluation
- eval once, call many: Compile to bytecode, execute bytecode
- Same for regex:
re.compile(pattern) then pattern.search()
- Lambda strings: Build lambda expression as string, eval it
- Handle errors: Division by zero, etc. still possible in calls
1---2name: compile-once-call-many3description: For hot loop optimization: repeated formula evaluation, regex patterns, expression compilation. Transform string to callable once, call many times.4---56# compile-once-call-many78## When to Use9- Same expression evaluated millions of times10- eval() or regex in a loop11- Formula/pattern is fixed, only values change12- Profiling shows string parsing as bottleneck1314## When NOT to Use15- Expression changes each iteration16- Only called a few times17- Code clarity more important than speed1819## The Pattern2021Transform string formula to compiled function, then call the function.2223```python24# SLOW: eval in loop25for values in million_combinations:26 if eval(f"{values[0]} + {values[1]} == {values[2]}"):27 results.append(values)2829# FAST: compile once, call many30formula = "lambda a, b, c: a + b == c"31check = eval(formula)32for values in million_combinations:33 if check(*values):34 results.append(values)35```3637## Example (from pytudes Cryptarithmetic.ipynb)3839```python40def solve(formula):41 """Slow version: eval in loop."""42 for digits in permutations('1234567890', len(letters)):43 filled = substitute(digits, letters, formula)44 if eval(filled): # eval called 3.6 million times!45 yield filled4647def faster_solve(formula):48 """Fast version: compile once, call many."""49 # Transform "NUM + BER = PLAY" to lambda50 fn_str, letters = translate_formula(formula)51 # fn_str = "lambda A,B,E,L,M,N,P,R,U,Y: (100*N+10*U+M) + (100*B+10*E+R) == ..."5253 formula_fn = eval(fn_str) # Compile once5455 for digits in permutations((1,2,3,4,5,6,7,8,9,0), len(letters)):56 try:57 if formula_fn(*digits): # Call compiled function58 yield format_solution(digits, letters, formula)59 except ArithmeticError:60 pass6162def translate_formula(formula):63 """Turn 'NUM + BER = PLAY' into evaluatable lambda."""64 letters = sorted(set(re.findall('[A-Z]', formula)))6566 # Convert words to arithmetic: NUM -> (100*N + 10*U + M)67 def word_to_expr(match):68 word = match.group()69 terms = [f"{10**(len(word)-i-1)}*{c}" for i, c in enumerate(word)]70 return f"({' + '.join(terms)})"7172 body = re.sub('[A-Z]+', word_to_expr, formula.replace('=', '=='))73 return f"lambda {','.join(letters)}: {body}", letters7475# Result: 15x speedup!76```7778## Key Principles791. **Profile first**: Confirm the bottleneck is evaluation802. **eval once, call many**: Compile to bytecode, execute bytecode813. **Same for regex**: `re.compile(pattern)` then `pattern.search()`824. **Lambda strings**: Build lambda expression as string, eval it835. **Handle errors**: Division by zero, etc. still possible in calls