代码之家  ›  专栏  ›  技术社区  ›  ken

如何最大限度地提高2元素组合的缓存命中率?

  •  2
  • ken  · 技术社区  · 2 年前

    我的问题很简单,但我发现很难把重点说清楚,所以请允许我一步一步地解释。

    假设我有 N 项目和 N 相应的指数。 每个项目都可以使用相应的索引加载。

    def load_item(index: int) -> ItemType:
        # Mostly just reading, but very slow.
        return item
    

    我还有一个函数,它获取两个(加载的)项目并计算分数。

    def calc_score(item_a: ItemType, item_b: ItemType) -> ScoreType:
        # Much faster than load function.
        return score
    

    请注意 calc_score(a, b) == calc_score(b, a) .

    我想做的是计算所有2项组合的分数,并找到(至少)一个给出最大分数的组合。

    这可以通过以下方式实现:

    def dumb_solution(n: int) -> Tuple[int, int]:
        best_score = 0
        best_combination = None
        for index_a, index_b in itertools.combinations(range(n), 2):
            item_a = load_item(index_a)
            item_b = load_item(index_b)
            score = calc_score(item_a, item_b)
            if score > best_score:
                best_score = score
                best_combination = (index_a, index_b)
        return best_combination
    

    但是,此解决方案调用 load_item 作用 2*C(N,2) = N*(N-1) 时间,这是该功能的瓶颈。

    这可以通过使用缓存来解决。 然而,不幸的是,这些项目太大了,不可能将所有项目都保存在内存中。 因此,我们需要使用大小有限的缓存。

    from functools import lru_cache
    
    @lru_cache(maxsize=M)
    def load(index: int) -> ItemType:
        # Very slow process.
        return item
    

    请注意 M (缓存大小)比 N (大约。 N // 10 N // 2 ).

    问题是,典型的组合序列对于LRU缓存来说并不理想。

    例如,当 N=6, M=3 , itertools.combinations 生成以下序列,以及的调用次数 加载项 函数为17。

    [
        (0, 1),  # 1, 2
        (0, 2),  # -, 3
        (0, 3),  # -, 4
        (0, 4),  # -, 5
        (0, 5),  # -, 6
        (1, 2),  # 7, 8
        (1, 3),  # -, 9
        (1, 4),  # -, 10
        (1, 5),  # -, 11
        (2, 3),  # 12, 13
        (2, 4),  # -, 14
        (2, 5),  # -, 15
        (3, 4),  # 16, 17
        (3, 5),  # -, -
        (4, 5),  # -, -
    ]
    

    但是,如果我按如下顺序重新排列以上序列,则调用次数将为10次。

    [
        (0, 1),  # 1, 2
        (0, 2),  # -, 3
        (1, 2),  # -, -
        (0, 3),  # -, 4
        (2, 3),  # -, -
        (0, 4),  # -, 5
        (3, 4),  # -, -
        (0, 5),  # -, 6
        (4, 5),  # -, -
        (1, 4),  # 7, -
        (1, 5),  # -, -
        (1, 3),  # -, 8
        (3, 5),  # -, -
        (2, 5),  # 9, -
        (2, 4),  # -, 10
    ]
    

    问题

    如何生成一个2项组合序列,以最大限度地提高缓存命中率?


    我尝试过的:

    我提出的解决方案是对缓存中已经存在的项目进行优先级排序。

    from collections import OrderedDict
    
    
    def prioritizes_item_already_in_cache(n, cache_size):
        items = list(itertools.combinations(range(n), 2))
        cache = OrderedDict()
        reordered = []
    
        def update_cache(x, y):
            cache[x] = cache[y] = None
            cache.move_to_end(x)
            cache.move_to_end(y)
            while len(cache) > cache_size:
                cache.popitem(last=False)
    
        while items:
            # Find a pair where both are cached.
            for i, (a, b) in enumerate(items):
                if a in cache and b in cache:
                    reordered.append((a, b))
                    update_cache(a, b)
                    del items[i]
                    break
            else:
                # Find a pair where one of them is cached.
                for i, (a, b) in enumerate(items):
                    if a in cache or b in cache:
                        reordered.append((a, b))
                        update_cache(a, b)
                        del items[i]
                        break
                else:
                    # Cannot find item in cache.
                    a, b = items.pop(0)
                    reordered.append((a, b))
                    update_cache(a, b)
    
        return reordered
    

    对于 N=100, M=10 ,该序列导致1660次调用,约为典型序列的1/3。对于 N=100, M=50 只有155个电话。所以我想我可以说这是一个很有希望的方法。

    不幸的是,这个功能太慢,对大型应用程序来说毫无用处 N . 我没能完成 N=1000 ,但实际数据是数以万计。 此外,它没有考虑在找不到缓存项目时如何选择项目。 因此,即使它很快,理论上它是否是最好的解决方案也是值得怀疑的(所以请注意,我的问题不是如何使上述功能更快)。

    (已编辑)这是完整的代码,包括每个人的答案以及测试和基准测试代码。

    import functools
    import itertools
    import math
    import time
    from collections import Counter, OrderedDict
    from itertools import chain, combinations, product
    from pathlib import Path
    from typing import Callable, Iterable, Tuple
    
    import joblib
    import matplotlib.pyplot as plt
    import numpy as np
    import pandas as pd
    from PIL import Image, ImageDraw
    
    ItemType = int
    ScoreType = int
    
    
    def load_item(index: int) -> ItemType:
        return int(index)
    
    
    def calc_score(item_a: ItemType, item_b: ItemType) -> ScoreType:
        return abs(item_a - item_b)
    
    
    class LRUCacheWithCounter:
        def __init__(self, maxsize: int):
            def wrapped_func(key):
                self.load_count += 1
                return load_item(key)
    
            self.__cache = functools.lru_cache(maxsize=maxsize)(wrapped_func)
            self.load_count = 0
    
        def __call__(self, key: int) -> int:
            return self.__cache(key)
    
    
    def basic_loop(iterator: Iterable[Tuple[int, int]], cached_load: Callable[[int], int]):
        best_score = 0
        best_combination = None
        for i, j in iterator:
            a = cached_load(i)
            b = cached_load(j)
            score = calc_score(a, b)
            if score > best_score:
                best_score = score
                best_combination = (i, j)
        return best_score, best_combination
    
    
    def baseline(n, _):
        return itertools.combinations(range(n), 2)
    
    
    def prioritizes(n, cache_size):
        items = list(itertools.combinations(range(n), 2))
        cache = OrderedDict()
        reordered = []
    
        def update_cache(x, y):
            cache[x] = cache[y] = None
            cache.move_to_end(x)
            cache.move_to_end(y)
            while len(cache) > cache_size:
                cache.popitem(last=False)
    
        while items:
            # Find a pair where both are cached.
            for i, (a, b) in enumerate(items):
                if a in cache and b in cache:
                    reordered.append((a, b))
                    update_cache(a, b)
                    del items[i]
                    break
            else:
                # Find a pair where one of them is cached.
                for i, (a, b) in enumerate(items):
                    if a in cache or b in cache:
                        reordered.append((a, b))
                        update_cache(a, b)
                        del items[i]
                        break
                else:
                    # Cannot find item in cache.
                    a, b = items.pop(0)
                    reordered.append((a, b))
                    update_cache(a, b)
    
        return reordered
    
    
    def Matt_solution(n: int, cache_size: int) -> Iterable[Tuple[int, int]]:
        dest = []
    
        def findPairs(lo1: int, n1: int, lo2: int, n2: int):
            if n1 < 1 or n2 < 1:
                return
            if n1 == 1:
                for i in range(max(lo1 + 1, lo2), lo2 + n2):
                    dest.append((lo1, i))
            elif n2 == 1:
                for i in range(lo1, min(lo1 + n1, lo2)):
                    dest.append((i, lo2))
            elif n1 >= n2:
                half = n1 // 2
                findPairs(lo1, half, lo2, n2)
                findPairs(lo1 + half, n1 - half, lo2, n2)
            else:
                half = n2 // 2
                findPairs(lo1, n1, lo2, half)
                findPairs(lo1, n1, lo2 + half, n2 - half)
    
        findPairs(0, n, 0, n)
        return dest
    
    
    def Kelly_solution(n: int, cache_size: int) -> Iterable[Tuple[int, int]]:
        k = cache_size // 2
        r = range(n)
        return chain.from_iterable(combinations(r[i : i + k], 2) if i == j else product(r[i : i + k], r[j : j + k]) for i in r[::k] for j in r[i::k])
    
    
    def Kelly_solution2(n: int, cache_size: int) -> Iterable[Tuple[int, int]]:
        k = cache_size - 2
        r = range(n)
        return chain.from_iterable(combinations(r[i : i + k], 2) if i == j else product(r[i : i + k], r[j : j + k]) for i in r[::k] for j in r[i::k])
    
    
    def diagonal_block(lower, upper):
        for i in range(lower, upper + 1):
            for j in range(i + 1, upper + 1):
                yield i, j
    
    
    def strip(i_lower, i_upper, j_lower, j_upper):
        for i in range(i_lower, i_upper + 1):
            for j in range(j_lower, j_upper + 1):
                yield i, j
    
    
    def btilly_solution(n: int, cache_size: int):
        i_lower = 0
        i_upper = n - 1
        k = cache_size - 2
        is_asc = True
        while i_lower <= i_upper:
            # Handle a k*k block first. At the end that is likely loaded.
            if is_asc:
                upper = min(i_lower + k - 1, i_upper)
                yield from diagonal_block(i_lower, upper)
                j_lower = i_lower
                j_upper = upper
                i_lower = upper + 1
            else:
                lower = max(i_lower, i_upper - k + 1)
                yield from diagonal_block(lower, i_upper)
                j_lower = lower
                j_upper = i_upper
                i_upper = lower - 1
            yield from strip(i_lower, i_upper, j_lower, j_upper)
            is_asc = not is_asc
    
    
    def btilly_solution2(n: int, cache_size: int):
        k = cache_size - 2
        for top in range(0, n, k):
            bottom = top + k
            # Diagonal part.
            for y in range(top, min(bottom, n)):  # Y-axis Top to Bottom
                for x in range(y + 1, min(bottom, n)):  # X-axis Left to Right
                    yield y, x
            # Strip part.
            # Stripping right to left works well when cache_size is very small, but makes little difference when it is not.
            for x in range(n - 1, bottom - 1, -1):  # X-axis Right to Left
                for y in range(top, min(bottom, n)):  # Y-axis Top to Bottom
                    yield y, x
    
    
    def btilly_solution3(n: int, cache_size: int):
        k = cache_size - 2
        r = range(n)
        for i in r[::k]:
            yield from combinations(r[i : i + k], 2)
            yield from product(r[i + k :], r[i : i + k])
    
    
    def btilly_solution4(n: int, cache_size: int):
        def parts():
            k = cache_size - 2
            r = range(n)
            for i in r[::k]:
                yield combinations(r[i : i + k], 2)
                yield product(r[i + k :], r[i : i + k])
    
        return chain.from_iterable(parts())
    
    
    def plot(df, series, ignore, y, label, title):
        df = df[df["name"].isin(series)]
        # plt.figure(figsize=(10, 10))
        for name, group in df.groupby("name"):
            plt.plot(group["n"], group[y], label=name)
    
        y_max = df[~df["name"].isin(ignore)][y].max()
        plt.ylim(0, y_max * 1.1)
    
        plt.xlabel("n")
        plt.ylabel(label)
        plt.title(title)
        plt.legend(loc="upper left")
        plt.tight_layout()
        plt.grid()
        plt.show()
    
    
    def run(func, n, cache_ratio, output_dir: Path):
        cache_size = int(n * cache_ratio / 100)
        output_path = output_dir / f"{n}_{cache_ratio}_{func.__name__}.csv"
        if output_path.exists():
            return
    
        started = time.perf_counter()
        for a, b in func(n, cache_size):
            pass
        elapsed_iterate = time.perf_counter() - started
    
        # test_combinations(func(n, cache_size), n)
    
        started = time.perf_counter()
        cache = LRUCacheWithCounter(cache_size)
        basic_loop(iterator=func(n, cache_size), cached_load=cache)
        elapsed_cache = time.perf_counter() - started
    
        output_path.write_text(f"{func.__name__},{n},{cache_ratio},{cache_size},{cache.load_count},{elapsed_iterate},{elapsed_cache}")
    
    
    def add_lower_bound(df):
        def calc_lower_bound(ni, mi):
            n = ni
            m = n * mi // 100
            return m + math.ceil((math.comb(n, 2) - math.comb(m, 2)) / (m - 1))
    
        return pd.concat(
            [
                df,
                pd.DataFrame(
                    [
                        {"name": "lower_bound", "n": ni, "m": mi, "count": calc_lower_bound(ni, mi)}
                        for ni, mi in itertools.product(df["n"].unique(), df["m"].unique())
                    ]
                ),
            ]
        )
    
    
    def benchmark(output_dir: Path):
        log_dir = output_dir / "log"
        log_dir.mkdir(parents=True, exist_ok=True)
    
        candidates = [
            baseline,
            prioritizes,
            Matt_solution,
            Kelly_solution,
            Kelly_solution2,
            btilly_solution,
            btilly_solution2,
            btilly_solution3,
            btilly_solution4,
        ]
    
        nc = np.linspace(100, 500, num=9).astype(int)
        # nc = np.linspace(500, 10000, num=9).astype(int)[1:]
        # nc = np.linspace(10000, 100000, num=9).astype(int).tolist()[1:]
        print(nc)
    
        mc = np.linspace(10, 50, num=2).astype(int)
        print(mc)
    
        joblib.Parallel(n_jobs=1, verbose=5, batch_size=1)([joblib.delayed(run)(func, ni, mi, log_dir) for ni in nc for mi in mc for func in candidates])
    
    
    def plot_graphs(output_dir: Path):
        log_dir = output_dir / "log"
    
        results = []
        for path in log_dir.glob("*.csv"):
            results.append(path.read_text().strip())
        (output_dir / "stat.csv").write_text("\n".join(results))
    
        df = pd.read_csv(output_dir / "stat.csv", header=None, names=["name", "n", "m", "size", "count", "time", "time_full"])
        df = add_lower_bound(df)
        df = df.sort_values(["name", "n", "m"])
    
        for m in [10, 50]:
            plot(
                df[df["m"] == m],
                series=[
                    baseline.__name__,
                    prioritizes.__name__,
                    Matt_solution.__name__,
                    Kelly_solution.__name__,
                    Kelly_solution2.__name__,
                    btilly_solution.__name__,
                    "lower_bound",
                ],
                ignore=[
                    baseline.__name__,
                    prioritizes.__name__,
                ],
                y="count",
                label="load count",
                title=f"cache_size = {m}% of N",
            )
    
        plot(
            df[df["m"] == 10],
            series=[
                baseline.__name__,
                prioritizes.__name__,
                Matt_solution.__name__,
                Kelly_solution.__name__,
                Kelly_solution2.__name__,
                btilly_solution.__name__,
                btilly_solution2.__name__,
                btilly_solution3.__name__,
                btilly_solution4.__name__,
            ],
            ignore=[
                prioritizes.__name__,
                Matt_solution.__name__,
            ],
            y="time",
            label="time (sec)",
            title=f"cache_size = {10}% of N",
        )
    
    
    class LRUCacheForTest:
        def __init__(self, maxsize: int):
            self.cache = OrderedDict()
            self.maxsize = maxsize
            self.load_count = 0
    
        def __call__(self, key: int) -> int:
            if key in self.cache:
                value = self.cache[key]
                self.cache.move_to_end(key)
            else:
                if len(self.cache) == self.maxsize:
                    self.cache.popitem(last=False)
                value = load_item(key)
                self.cache[key] = value
                self.load_count += 1
            return value
    
        def hit(self, i, j):
            count = int(i in self.cache)
            self(i)
            count += int(j in self.cache)
            self(j)
            return count
    
    
    def visualize():
        # Taken from https://stackoverflow.com/a/77024514/18125313 and modified.
        n, m = 100, 30
        func = btilly_solution2
    
        pairs = func(n, m)
        cache = LRUCacheForTest(m)
    
        # Create the images, save as animated png.
        images = []
        s = 5
        img = Image.new("RGB", (s * n, s * n), (255, 255, 255))
        draw = ImageDraw.Draw(img)
    
        colors = [(255, 0, 0), (255, 255, 0), (0, 255, 0)]
        for step, (i, j) in enumerate(pairs):
            draw.rectangle((s * j, s * i, s * j + s - 2, s * i + s - 2), colors[cache.hit(i, j)])
            if not step % 17:
                images.append(img.copy())
    
        images += [img] * 40
    
        images[0].save(f"{func.__name__}_{m}.gif", save_all=True, append_images=images[1:], optimize=False, duration=30, loop=0)
    
    
    def test_combinations(iterator: Iterable[Tuple[int, int]], n: int):
        # Note that this function is not suitable for large N.
        expected = set(frozenset(pair) for pair in itertools.combinations(range(n), 2))
        items = list(iterator)
        actual = set(frozenset(pair) for pair in items)
        assert len(actual) == len(items), f"{[item for item, count in Counter(items).items() if count > 1]}"
        assert actual == expected, f"dup={actual - expected}, missing={expected - actual}"
    
    
    def test():
        n = 100  # N
        cache_size = 30  # M
    
        def run(func):
            func(n, cache_size)
    
            # Measure generation performance.
            started = time.perf_counter()
            for a, b in func(n, cache_size):
                pass
            elapsed = time.perf_counter() - started
    
            # Test generated combinations.
            test_combinations(func(n, cache_size), n)
    
            # Measure cache hit (load count) performance.
            cache = LRUCacheWithCounter(cache_size)
            _ = basic_loop(iterator=func(n, cache_size), cached_load=cache)
            print(f"{func.__name__}: {cache.load_count=}, {elapsed=}")
    
        candidates = [
            baseline,
            prioritizes,
            Matt_solution,
            Kelly_solution,
            Kelly_solution2,
            btilly_solution,
            btilly_solution2,
            btilly_solution3,
            btilly_solution4,
        ]
        for f in candidates:
            run(f)
    
    
    def main():
        test()
        visualize()
    
        output_dir = Path("./temp2")
        benchmark(output_dir)
        plot_graphs(output_dir)
    
    
    if __name__ == "__main__":
        main()
    

    我对你不使用上面的测试代码或更改的行为没有问题 basic_loop LRUCacheWithCounter .

    附加说明:

    • 不能使用邻居分数来修剪分数计算。
    • 不能仅使用项目的一部分来修剪分数计算。
    • 无法猜测最佳组合在哪里。
    • 使用更快的媒体是一种选择,但我已经达到了极限,所以我正在寻找软件解决方案。

    谢谢你把这篇长文读到最后。


    编辑

    多亏了btilly的回答和Kelly可视化的帮助,我得出结论,btilly解决方案是最好的,(可能)是最优的。

    这里有一个理论解释(虽然我数学不太好,所以可能是错的)。


    允许 N 表示索引的数量, M 缓存大小,以及 C 组合的数量(与 math.comb ).

    考虑缓存已满并且 无法生成进一步的组合 无负载。 如果我们在这一点上添加一个新索引,那么唯一可以生成的组合就是新添加的索引和缓存中剩余索引的组合。 这种模式适用于后续的每次迭代。 因此,当缓存已满时,每次加载可以生成的最大组合数为 M - 1 .

    如果缓存未满,则此逻辑也适用。 如果 M' 索引当前在缓存中,则下一个索引最多可以生成 M 组合。 后续索引最多可以生成 M' + 1 组合等等。 总共,最多 C(M,2) 可以在缓存满之前生成组合。

    因此,为了生成 C(N,2) 组合,至少 M 至少需要加载才能填充缓存 (C(N,2) - C(M,2)) / (M - 1) 在缓存被填充之后需要加载。

    从上面来看,负载计数这个问题的复杂性是 Ω(N^2 / M) .


    我将这个公式绘制为 lower_bound 在下图中。 请注意,这只是一个下限,并不能保证它能够实际实现。

    顺便说一句,Kelly的解决方案需要配置 k 以最大限度地提高其性能。 对于 M = 50% of N ,是关于 M * 2/3 . 对于 M = 30% of N ,是关于 M * 5/6 . 虽然我不知道怎么算。 作为一般配置,我使用 k = M - 2 (这不是最好的,但相对较好) Kelly_solution2 在下图中。

    对于 M = 10% of N :

    n_to_load_count_graph_10

    对于 M=N的50% :

    n_to_load_count_graph_50

    请注意,在这些图中,它看起来像 O(N) ,但这是因为我下定决心 M 基于 N 什么时候 M 不会改变,是的 O(N^2) 如上所述。

    这是一个可视化的缓存命中率的动画 btilly_solution2 ,由凯利代码的修改版本组成。 每个像素表示一个组合,红色表示加载了两个索引的组合,黄色表示加载了一个索引,绿色表示两个索引都未加载。

    visualization_of_btilly_solution2

    此外,由于我在寻找最佳序列,所以执行时间并不重要。 但为了防止有人好奇,这里有一个执行时间的比较(仅限迭代)。

    n_to_time_graph

    btilly_solution4 (凯利修改的btilly的解)几乎和 itertools.com组合 ,在这种情况下应该是最优的。 然而,请注意,即使没有修改,每次组合也只需要112纳秒。

    就这样。感谢所有参与其中的人。

    0 回复  |  直到 2 年前
        1
  •  11
  •   btilly    2 年前

    这里有一个简单的方法,它依赖于缓存,并在基准测试中获得230。

    def diagonal_block (lower, upper):
        for i in range(lower, upper + 1):
            for j in range(i, upper + 1):
                yield (i, j)
    
    def strip (i_lower, i_upper, j_lower, j_upper):
        for i in range(i_lower, i_upper+1):
            for j in range (j_lower, j_upper + 1):
                yield (i, j)
    
    # def your_solution_here(n: int, cache_size: int) -> Iterable[Tuple[int, int]]:
    def your_solution_here(n: int, cache_size: int):
        i_lower = 0
        i_upper = n-1
        k = cache_size - 2
        is_asc = True
        while i_lower <= i_upper:
            # Handle a k*k block first. At the end that is likely loaded.
            if is_asc:
                upper = min(i_lower + k - 1, i_upper)
                yield from diagonal_block(i_lower, upper)
                j_lower = i_lower
                j_upper = upper
                i_lower = upper + 1
            else:
                lower = max(i_lower, i_upper - k + 1)
                yield from diagonal_block(lower, i_upper)
                j_lower = lower
                j_upper = i_upper
                i_upper = lower - 1
            yield from strip(i_lower, i_upper, j_lower, j_upper)
            is_asc = not is_asc
    

    关于我是如何想到这一点的评论。

    我们想将一组对象与其他未共享的对象进行比较。该组应该是除一个之外适合缓存的所有内容。

    所以我们从第一个开始 k 对象,将它们相互比较,然后以条状进行到底。

    现在我们需要第二组。好吧,我们已经有了最后一个对象,我们不需要剩下的。所以我们采取 k 对象,使其成为一个组。将组与自身进行比较,然后沿着条形前进到原始组之外的第一个对象。

    现在反转方向,依此类推。

    在所有点上, i_lower 表示仍需要比较的第一个对象,以及 i_upper 代表最后一个。如果我们继续前进,我们采取 k 对象开始于 _降低 .如果我们要倒退,我们就采取 k 对象开始于 上(_O) 然后倒退。

    当我实施它的时候,有两个复杂的问题。第一,当我们遇到在中间时,我们必须担心边缘条件。第二个是,我们可能必须在两个方向上进行剥离。

    我选择了只做脱衣舞。这实际上是一个bug。在大多数递增加载中,我没有得到缓存中的第一个元素。哎呀。但它仍然很好。

        2
  •  7
  •   Kelly Bundy    2 年前

    进入kk街区。当k=25时,它在基准测试上获得372个加载。当k=cache_size/2时,它获得570个加载。

    from itertools import combinations, product, chain
    
    def your_solution_here(n: int, cache_size: int) -> Iterable[Tuple[int, int]]:
        k = cache_size // 2
        r = range(n)
        return list(chain.from_iterable(
            combinations(r[i:i+k], 2) if i == j else product(r[i:i+k], r[j:j+k])
            for i in r[::k]
            for j in r[i::k]
        ))
    
        3
  •  5
  •   Matt Timmermans    2 年前

    这里有一个简单的递归定义的排序,它不依赖于缓存大小,并在基准测试上获得566个加载:

    def cache_oblivious(n: int, cache_size: int) -> Iterable[Tuple[int, int]]:
        dest = []
        def findPairs(lo1: int, n1: int, lo2: int, n2: int):
            if n1 < 1 or n2 < 1:
                return
            if n1 == 1:
                for i in range(max(lo1+1,lo2), lo2+n2):
                    dest.append((lo1, i))
            elif n2 == 1:
                for i in range(lo1, min(lo1+n1, lo2)):
                    dest.append((i, lo2))
            elif n1 >= n2:
                half = n1//2
                findPairs(lo1, half, lo2, n2)
                findPairs(lo1+half, n1-half, lo2, n2)
            else:
                half = n2//2
                findPairs(lo1, n1, lo2, half)
                findPairs(lo1, n1, lo2+half, n2-half)
        findPairs(0,n,0,n)
        return dest
    
        4
  •  2
  •   Kelly Bundy    2 年前

    n=100,cache_size=30的可视化

    肯的 baseline ,4614个负载: enter image description here

    肯的 prioritizes_item_already_in_cache ,963个负载: enter image description here

    My first one ,570个负载: enter image description here

    Matt's ,565个负载: enter image description here

    btilly's ,230个负载: enter image description here

    肯的 btilly_solution2 ,226个负载: enter image description here

    My btilly variation ,231个负载: enter image description here

    图像创建代码

    from PIL import Image, ImageDraw
    from typing import Callable, Iterable, Tuple
    from itertools import combinations, product, chain
    
    n, m = 100, 30
    
    def your_solution_here(n: int, cache_size: int) -> Iterable[Tuple[int, int]]:
        k = cache_size // 2
        r = range(n)
        return list(chain.from_iterable(
            combinations(r[i:i+k], 2) if i == j else product(r[i:i+k], r[j:j+k])
            for i in r[::k]
            for j in r[i::k]
        ))
    
    '''
    from btilly import your_solution_here
    from Matt import your_solution_here
    from ken import baseline as your_solution_here
    from ken import prioritizes_item_already_in_cache as your_solution_here
    '''
    
    pairs = your_solution_here(n, m)
    
    # Create the images, save as animated png.
    images = []
    s = 5
    img = Image.new('RGB', (s*n, s*n), (255, 255, 255))
    draw = ImageDraw.Draw(img)
    color = 255,0,0
    for step, (i, j) in enumerate(pairs):
        draw.rectangle([s*j, s*i, s*j+s-2, s*i+s-2], color)
        if not step % 17:
            images.append(img.copy())
    
    images += [img] * 40
    
    images[0].save('foo.png', save_all=True, append_images=images[1:],
                   optimize=False, duration=30, loop=0)
    
    print(len(images))
    print('Done, see the created foo.png image.')
    
        5
  •  1
  •   Kelly Bundy    2 年前

    btilly的变体,在您的基准上获得231:

    from itertools import combinations, product, chain
    
    def your_solution_here(n: int, cache_size: int):
        k = cache_size - 2
        r = range(n)
        for i in r[::k]:
            yield from combinations(r[i:i+k], 2)
            yield from product(r[i+k:], r[i:i+k])
    

    速度优化版本:

    def your_solution_here(n: int, cache_size: int):
        def parts():
            k = cache_size - 2
            r = range(n)
            for i in r[::k]:
                yield combinations(r[i:i+k], 2)
                yield product(r[i+k:], r[i:i+k])
        return chain.from_iterable(parts())