⚡️ Speed up function memoize_last_value by 7%
#617
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
📄 7% (0.07x) speedup for
memoize_last_valueinmarimo/_utils/memoize.py⏱️ Runtime :
20.4 microseconds→19.1 microseconds(best of213runs)📝 Explanation and details
The optimization achieves a 6% speedup by eliminating redundant data structure creation and improving the cache hit comparison logic.
What specific optimizations were applied:
Separate variable storage: Instead of storing inputs as a single tuple
(args, frozenset(kwargs.items())), the optimized version uses separate variableslast_input_argsandlast_input_kwargs. This avoids creating a new tuple wrapper on every function call.More efficient argument comparison: Replaced the manual index-based loop with
zip(args, last_input_args)and a generator expression. Thezipapproach is more Pythonic and can short-circuit earlier when arguments don't match.Deferred frozenset creation: The original version created
frozenset(kwargs.items())unconditionally on every call. The optimized version only creates it when needed for comparison or storage, reducing allocations when kwargs are empty or unchanged.Why this leads to speedup:
zip-based comparison can exit early on the first argument mismatchTest case performance patterns:
The optimizations show consistent 3-15% improvements across test cases, with the best gains in:
This memoization decorator is particularly useful for expensive computations where the same objects are passed repeatedly, making these micro-optimizations valuable for performance-critical code paths.
✅ Correctness verification report:
⚙️ Existing Unit Tests and Runtime
🌀 Generated Regression Tests and Runtime
from future import annotations
from typing import Any, Callable, TypeVar, cast
imports
import pytest # used for our unit tests
from marimo._utils.memoize import memoize_last_value
function to test
Copyright 2024 Marimo. All rights reserved.
T = TypeVar("T")
sentinel = object() # Unique sentinel object
from marimo._utils.memoize import memoize_last_value
unit tests
----- BASIC TEST CASES -----
def test_basic_identity_positional_args():
"""Test memoization with identical positional arguments by identity."""
calls = []
@memoize_last_value
def f(x):
calls.append(x)
return x * 2
def test_basic_multiple_args():
"""Test memoization with multiple positional arguments."""
calls = []
@memoize_last_value
def f(x, y):
calls.append((x, y))
return x + y
def test_basic_kwargs_order_irrelevant():
"""Test that kwargs order does not affect memoization."""
calls = []
@memoize_last_value
def f(x, y=0, z=0):
calls.append((x, y, z))
return x + y + z
----- EDGE TEST CASES -----
def test_edge_no_args():
"""Test memoization for functions with no arguments."""
calls = []
@memoize_last_value
def f():
calls.append(1)
return 42
def test_edge_none_argument():
"""Test memoization with None as argument."""
calls = []
@memoize_last_value
def f(x):
calls.append(x)
return x
def test_edge_mutable_argument_identity():
"""Test memoization with mutable objects (list) by identity."""
calls = []
@memoize_last_value
def f(x):
calls.append(x)
return x[:]
def test_edge_mutable_argument_different_identity():
"""Test memoization with different mutable object instances."""
calls = []
@memoize_last_value
def f(x):
calls.append(x)
return x[:]
def test_edge_different_number_of_args():
"""Test memoization when argument count changes."""
calls = []
@memoize_last_value
def f(*args):
calls.append(args)
return sum(args)
def test_edge_different_kwargs():
"""Test memoization when kwargs change."""
calls = []
@memoize_last_value
def f(x, y=0):
calls.append((x, y))
return x + y
def test_edge_argument_is_sentinel():
"""Test memoization when argument is the sentinel object itself."""
calls = []
@memoize_last_value
def f(x):
calls.append(x)
return x
def test_edge_kwargs_empty_vs_missing():
"""Test memoization with missing vs. empty kwargs."""
calls = []
@memoize_last_value
def f(x, y=0):
calls.append((x, y))
return x + y
def test_edge_large_tuple_args():
"""Test memoization with large tuple argument."""
calls = []
@memoize_last_value
def f(x):
calls.append(x)
return sum(x)
----- LARGE SCALE TEST CASES -----
def test_large_scale_many_calls_same_object():
"""Test memoization for many repeated calls with same object."""
calls = []
@memoize_last_value
def f(x):
calls.append(x)
return x + 1
def test_large_scale_many_calls_different_objects():
"""Test memoization for many calls with different objects."""
calls = []
@memoize_last_value
def f(x):
calls.append(x)
return x * 2
def test_large_scale_large_list_argument():
"""Test memoization with large list argument."""
calls = []
@memoize_last_value
def f(lst):
calls.append(lst)
return sum(lst)
def test_large_scale_large_kwargs():
"""Test memoization with large number of kwargs."""
calls = []
@memoize_last_value
def f(**kwargs):
calls.append(kwargs)
return sum(kwargs.values())
def test_large_scale_multiple_args_and_kwargs():
"""Test memoization with many args and kwargs."""
calls = []
@memoize_last_value
def f(*args, **kwargs):
calls.append((args, kwargs))
return sum(args) + sum(kwargs.values())
codeflash_output is used to check that the output of the original code is the same as that of the optimized code.
#------------------------------------------------
from typing import Any, Callable, TypeVar, cast
imports
import pytest
from marimo._utils.memoize import memoize_last_value
T = TypeVar("T")
sentinel = object() # Unique sentinel object
from marimo._utils.memoize import memoize_last_value
unit tests
----------- BASIC TEST CASES -----------
def test_basic_memoization_with_primitives():
"""Test that memoize_last_value returns cached result for same primitive args by identity."""
call_counter = {"count": 0}
def add(a, b):
call_counter["count"] += 1
return a + b
codeflash_output = memoize_last_value(add); memoized_add = codeflash_output # 887ns -> 827ns (7.26% faster)
# Third call with new objects (same value, but different int objects): should compute
x = int(1)
y = int(2)
# For small ints, Python may reuse objects, but for test robustness, check identity
# If x is 1 and y is 2, they may be the same object as previous, so call_counter may not increment
# So we force new objects for non-interned types below
def test_basic_memoization_with_mutable_objects():
"""Test that memoize_last_value uses object identity for mutable objects."""
call_counter = {"count": 0}
def concat(lst):
call_counter["count"] += 1
return lst + [1]
codeflash_output = memoize_last_value(concat); memoized_concat = codeflash_output # 922ns -> 875ns (5.37% faster)
lst = [0]
# Third call with different but equal object: should compute
lst2 = [0]
def test_basic_memoization_with_kwargs():
"""Test that memoize_last_value uses keyword arguments in cache key."""
call_counter = {"count": 0}
def foo(a, b=2):
call_counter["count"] += 1
return a + b
codeflash_output = memoize_last_value(foo); memoized_foo = codeflash_output # 884ns -> 838ns (5.49% faster)
def test_basic_memoization_with_no_args():
"""Test memoization for functions with no arguments."""
call_counter = {"count": 0}
def get_value():
call_counter["count"] += 1
return 42
codeflash_output = memoize_last_value(get_value); memoized_get_value = codeflash_output # 891ns -> 829ns (7.48% faster)
----------- EDGE TEST CASES -----------
def test_edge_different_length_args():
"""Test that cache is not hit if argument lengths differ."""
call_counter = {"count": 0}
def foo(*args):
call_counter["count"] += 1
return sum(args)
codeflash_output = memoize_last_value(foo); memoized_foo = codeflash_output # 917ns -> 846ns (8.39% faster)
def test_edge_kwargs_order_irrelevant():
"""Test that kwargs order does not affect caching."""
call_counter = {"count": 0}
def foo(a, b=1, c=2):
call_counter["count"] += 1
return a + b + c
codeflash_output = memoize_last_value(foo); memoized_foo = codeflash_output # 896ns -> 838ns (6.92% faster)
def test_edge_object_identity_vs_equality():
"""Test that only object identity matters, not equality."""
call_counter = {"count": 0}
def foo(a):
call_counter["count"] += 1
return a
codeflash_output = memoize_last_value(foo); memoized_foo = codeflash_output # 877ns -> 851ns (3.06% faster)
x = [1]
y = [1]
def test_edge_mutable_argument_change():
"""Test that changing the mutable argument does not affect cache key if object is same."""
call_counter = {"count": 0}
def foo(a):
call_counter["count"] += 1
return sum(a)
codeflash_output = memoize_last_value(foo); memoized_foo = codeflash_output # 930ns -> 872ns (6.65% faster)
x = [1, 2]
x.append(3) # Mutate the list
def test_edge_none_argument():
"""Test that None as argument works and is cached by identity."""
call_counter = {"count": 0}
def foo(a):
call_counter["count"] += 1
return a is None
codeflash_output = memoize_last_value(foo); memoized_foo = codeflash_output # 879ns -> 825ns (6.55% faster)
def test_edge_multiple_calls_with_different_objects():
"""Test that cache is only hit for the immediately previous call with same objects."""
call_counter = {"count": 0}
def foo(a):
call_counter["count"] += 1
return a
codeflash_output = memoize_last_value(foo); memoized_foo = codeflash_output # 837ns -> 818ns (2.32% faster)
x = [1]
y = [2]
def test_edge_kwargs_and_args_identity():
"""Test that cache key includes both positional and keyword arguments by identity."""
call_counter = {"count": 0}
def foo(a, b=2):
call_counter["count"] += 1
return a + b
codeflash_output = memoize_last_value(foo); memoized_foo = codeflash_output # 881ns -> 853ns (3.28% faster)
# New object for positional argument
x = int(1)
# For small ints, identity may be the same, so call_counter may not increment
def test_edge_function_with_various_types():
"""Test memoization with various argument types."""
call_counter = {"count": 0}
def foo(a, b, c=None):
call_counter["count"] += 1
return (a, b, c)
codeflash_output = memoize_last_value(foo); memoized_foo = codeflash_output # 885ns -> 831ns (6.50% faster)
a = [1]
b = {"x": 2}
c = (3,)
# Change positional argument object
a2 = [1]
def test_edge_function_with_no_return():
"""Test memoization for functions that return None."""
call_counter = {"count": 0}
def foo():
call_counter["count"] += 1
# No return statement
codeflash_output = memoize_last_value(foo); memoized_foo = codeflash_output # 905ns -> 790ns (14.6% faster)
----------- LARGE SCALE TEST CASES -----------
def test_large_scale_many_calls_and_args():
"""Test memoization efficiency and correctness with many unique arguments."""
call_counter = {"count": 0}
def foo(x):
call_counter["count"] += 1
return x * 2
codeflash_output = memoize_last_value(foo); memoized_foo = codeflash_output # 888ns -> 777ns (14.3% faster)
# Create 1000 unique objects
objs = [object() for _ in range(1000)]
for i in range(1000):
pass
def test_large_scale_repeated_calls_same_object():
"""Test that repeated calls with same object are cached, even in large scale."""
call_counter = {"count": 0}
def foo(x):
call_counter["count"] += 1
return x + 1
codeflash_output = memoize_last_value(foo); memoized_foo = codeflash_output # 872ns -> 769ns (13.4% faster)
x = 42
for i in range(100):
pass
# Only first call increments counter
def test_large_scale_with_mutable_objects():
"""Test memoization with many mutable objects."""
call_counter = {"count": 0}
def foo(lst):
call_counter["count"] += 1
return sum(lst)
codeflash_output = memoize_last_value(foo); memoized_foo = codeflash_output # 876ns -> 809ns (8.28% faster)
lists = [[i] for i in range(500)]
for i in range(500):
pass
def test_large_scale_args_and_kwargs():
"""Test memoization with both args and kwargs in large scale."""
call_counter = {"count": 0}
def foo(a, b=0):
call_counter["count"] += 1
return a + b
codeflash_output = memoize_last_value(foo); memoized_foo = codeflash_output # 919ns -> 846ns (8.63% faster)
for i in range(200):
pass
def test_large_scale_changing_objects():
"""Test that cache is only hit for immediate previous call, not for earlier calls."""
call_counter = {"count": 0}
def foo(x):
call_counter["count"] += 1
return x
codeflash_output = memoize_last_value(foo); memoized_foo = codeflash_output # 873ns -> 829ns (5.31% faster)
objs = [object() for _ in range(20)]
for i in range(20):
pass
# Previous call is always a new object, so no cache hit
codeflash_output is used to check that the output of the original code is the same as that of the optimized code.
#------------------------------------------------
from marimo._utils.memoize import memoize_last_value
def test_memoize_last_value():
memoize_last_value(lambda *a: 0)
⏪ Replay Tests and Runtime
test_pytest_tests_serverapitest_auth_py_testscodeflash_concolic_xzlu4vu2tmpz0obg9nztest_concolic_coverage__replay_test_0.py::test_marimo__utils_memoize_memoize_last_valuetest_pytest_tests_utilstest_narwhals_utils_py_tests_pluginsui_impltablestest_format_py_tests_pluginsstate__replay_test_0.py::test_marimo__utils_memoize_memoize_last_value🔎 Concolic Coverage Tests and Runtime
codeflash_concolic_bps3n5s8/tmpehq92g0h/test_concolic_coverage.py::test_memoize_last_valueTo edit these changes
git checkout codeflash/optimize-memoize_last_value-mhvkdwf5and push.