△△請給“Python貓”加星標 ,以免錯過文章推送

打開網(wǎng)易新聞 查看精彩圖片

英文: https://minimaxir.com/2025/01/write-better-code

作者:Max Woolf

翻譯:Python貓

2023 年 11 月,OpenAI為 ChatGPT 添加了新功能[1],讓用戶可以在網(wǎng)頁中使用 DALL-E 3 生成圖像。隨后出現(xiàn)了一個短暫的網(wǎng)絡(luò)梗[2]:用戶會給 LLM(大語言模型)一張基礎(chǔ)圖片,然后不斷要求它"讓圖片更 X",這里的 X 可以是任何特征或風(fēng)格。

打開網(wǎng)易新聞 查看精彩圖片
A regular guy becomes more "bro" every time. via /u/Jojop0tato on Reddit.
打開網(wǎng)易新聞 查看精彩圖片
Asked ChatGPT to make Santa Claus more and more serious. via /u/hessihan on Reddit.

這個趨勢很快就消失了,因為生成的圖像都過于相似,缺乏新意。有趣的是,無論初始圖像和提示詞如何不同,最終的結(jié)果都會趨向于某種"宇宙化"的效果。這種現(xiàn)象在"AI 垃圾"(AI slop)這一術(shù)語被正式定義之前,就已經(jīng)是典型的AI 垃圾[3]了。不過從學(xué)術(shù)角度來看,即使是這些看似無意義和模糊的提示詞,也能對最終圖像產(chǎn)生明顯的、可預(yù)測的影響,這一點仍然值得玩味。

如果我們對代碼用類似的方法會怎樣呢?由于代碼需要遵循嚴格的規(guī)則,而且與圖像等創(chuàng)意輸出不同,代碼質(zhì)量可以更客觀地衡量,所以 LLM 生成的代碼不太可能是垃圾(盡管也不是完全不可能[4])。

如果代碼真的可以通過簡單的迭代提示來改進,比如僅僅要求 LLM “讓代碼更好”(雖然這聽起來很傻),這將帶來巨大的生產(chǎn)力提升。那么,如果不斷這樣迭代下去會發(fā)生什么?代碼會出現(xiàn)什么樣的"宇宙化"效果?讓我們來一探究竟!

與 LLM 隨意編碼

盡管在 ChatGPT 出現(xiàn)之前我就一直在研究和開發(fā) LLM 相關(guān)工具,但我并不喜歡使用 LLM 代碼協(xié)助工具(如 GitHub Copilot)來輔助編碼。在"哦,LLM 自動完成了我的代碼,很好"/"我應(yīng)該問 LLM 什么問題"和"LLM 生成的代碼真的正確嗎,它是否在產(chǎn)生幻覺[5]并生成看似正確的代碼"之間不斷切換思維,這種持續(xù)的心智負擔抵消了使用 AI 帶來的任何生產(chǎn)力提升。這還沒有考慮使用這些 LLM 的昂貴成本。

Claude 3.5 Sonnet[6]讓我重新思考這個問題。由于 Anthropic 在訓(xùn)練中使用的某種秘密配方,最新版本的 Claude 3.5 Sonnet (claude-3-5-sonnet-20241022) 對各類提示詞都有著令人難以置信的遵循能力,尤其是在編碼提示方面。編碼[7]基準測試[8]證實,在 Claude 3.5 Sonnet 和 GPT-4 的測試中,Claude 勝出,而且根據(jù)我在各種技術(shù)和創(chuàng)意任務(wù)中的親身經(jīng)歷,也得出了相同的結(jié)論。

初始嘗試

在這個實驗中,我們將給 Claude 3.5 Sonnet 一個面試風(fēng)格的 Python 編碼提示詞:一個簡單適合被新手軟件工程師實現(xiàn),但同時又可以進行大量優(yōu)化的問題。這個簡單隨意的提示詞代表了典型軟件工程師使用 LLM 的方式。此外,測試提示詞必須完全原創(chuàng),而不是取自 LeetCode 或 HackerRank 等編碼測試平臺,因為 LLM 很可能在這些平臺上進行過訓(xùn)練,可能會通過背誦記憶的答案作弊。

完整的、未經(jīng)編輯的基于這種隨意提示的對話內(nèi)容可在 GitHub 上獲取[9]。

這是我自己寫的測試提示詞:

編寫 Python 代碼來解決這個問題:

給定一個包含 100 萬個 1 到 100,000 之間的隨機整數(shù)的列表,找出數(shù)字之和等于 30 的最大數(shù)和最小數(shù)之間的差值。

將其作為用戶提示詞給 Claude API,并設(shè)置 temperature=0 以獲得最佳/最確定的答案,最終實現(xiàn)如下:

import random

def digit_sum(n):
    """Calculate the sum of digits of a number"""
    return sum(int(digit) for digit in str(n))

def find_difference():
     # Generate list of 1 million random integers 
    numbers = [random.randint(1, 100000) for _ in range(1000000)]

     # Initialize variables for min and max numbers with digit sum 30 
    min_num = float('inf')   # Initialize to positive infinity 
    max_num = float('-inf')   # Initialize to negative infinity 

     # Find numbers whose digits sum to 30 
    for num in numbers:
        if digit_sum(num) == 30:
            min_num = min(min_num, num)
            max_num = max(max_num, num)

     # Check if we found any numbers with digit sum 30 
    if min_num == float('inf') or max_num == float('-inf'):
        return "No numbers found with digit sum of 30"

    return max_num - min_num

這個實現(xiàn)是正確的,并且與大多數(shù) Python 新手程序員的寫法一致,還額外處理了沒有符合條件的數(shù)字的情況。對于列表中的每個數(shù)字,檢查其數(shù)字之和是否為 30:如果是,則檢查它是否大于最近看到的最大數(shù)字或小于最近看到的最小數(shù)字,并相應(yīng)地更新這些變量。搜索完列表后,返回差值。

但我敢肯定,許多閱讀這個實現(xiàn)的程序員都會思考如何優(yōu)化。一個例子是digit_sum()函數(shù):雖然這個實現(xiàn)是一個有趣的 Python 一行代碼,但在strint之間的類型轉(zhuǎn)換會造成大量不必要的開銷。

在我的 M3 Pro MacBook Pro 上,這段代碼平均需要 657 毫秒運行。我們將使用這個性能作為基準,來比較后續(xù)實現(xiàn)的測試結(jié)果(劇透:它們都更快)。

迭代 #1

現(xiàn)在我們可以讓 Claude 改進代碼,將其當前答案和之前的所有內(nèi)容作為對話提示的一部分。因此,我們添加迭代提示:

寫更好的代碼

你以為這篇博文的標題是在開玩笑嗎?我從不開玩笑。

Claude 輸出了修改后的代碼,用它的話說是"代碼的優(yōu)化版本,有幾處改進"。它沒有將代碼保持為函數(shù)形式,而是重構(gòu)為 Python 類,使其更加面向?qū)ο螅?/p>

class DigitSumFinder:
    def __init__(self, target_sum: int = 30,                  range_start: int = 1,                  range_end: int = 100_000,                  count: int = 1_000_000):
        self.target_sum = target_sum
        self.range_start = range_start
        self.range_end = range_end
        self.count = count

         # Pre-calculate digit sums for all possible numbers 
        self.digit_sums = self._precompute_digit_sums()

    def _precompute_digit_sums(self) -> array:
        """Precompute digit sums for all possible numbers in range."""
        digit_sums = array('B', [0] * (self.range_end + 1))
        for num in range(self.range_start, self.range_end + 1):
            total = 0
            n = num
            while n:
                total += n % 10
                n //= 10
            digit_sums[num] = total
        return digit_sums

    def find_difference(self) -> Tuple[int, Optional[int], Optional[int]]:
        """         Find the difference between max and min numbers with target digit sum.         Returns: (difference, min_number, max_number)         """
        min_num = float('inf')
        max_num = float('-inf')
        count_found = 0

         # Generate and process random numbers 
        for _ in range(self.count):
            num = random.randint(self.range_start, self.range_end)
            if self.digit_sums[num] == self.target_sum:
                count_found += 1
                if num < min_num:
                    min_num = num
                if num > max_num:
                    max_num = num

        if count_found == 0:
            return 0, None, None

        return max_num - min_num, min_num, max_num

這里,代碼做了兩個智能的算法改進:

  • 在計算數(shù)字和時使用整數(shù)運算,避免了前面提到的類型轉(zhuǎn)換的需要。

  • 預(yù)計算所有可能的數(shù)字和并將它們存儲在字節(jié)數(shù)組中(使用數(shù)組而不是列表有點不尋常)以供查找,這意味著當一百萬個數(shù)字列表中出現(xiàn)重復(fù)數(shù)字時,不需要重新計算數(shù)字和。由于這個數(shù)組作為類的字段存儲,在搜索新的隨機數(shù)列表時不需要重新計算。

這些優(yōu)化使代碼速度相對于初始實現(xiàn)提高了 2.7 倍。

迭代 #2

再次提示寫更好的代碼,Claude 發(fā)現(xiàn)了更多明顯的優(yōu)化(為了可讀性進行了截斷):

class DigitSumAnalyzer:
    """Analyzes numbers based on their digit sums."""

    def __init__(self, number_range: NumberRange):
        self.number_range = number_range
        self._digit_sums = self._precompute_digit_sums()

    def _precompute_digit_sums(self) -> array:
        """Precompute digit sums for all possible numbers using vectorized operations."""
        digits = np.arange(self.number_range.end + 1)
        digit_sums = np.zeros(self.number_range.end + 1, dtype=np.uint8)

        while digits.any():
            digit_sums += digits % 10
            digits //= 10

        return array('B', digit_sums)

    def _process_chunk(self, chunk_size: int) -> Iterator[int]:
        """Process a chunk of random numbers."""
        numbers = np.random.randint(
            self.number_range.start,
            self.number_range.end + 1,
            chunk_size,
            dtype=np.int32
        )
        mask = np.frombuffer(self._digit_sums, dtype=np.uint8)[numbers] == self.number_range.target_sum
        yield from numbers[mask]

    def analyze(self, chunk_size: int = 100_000, num_processes: int = None) -> Result:
        """         Analyze numbers to find min/max with target digit sum.         Args:             chunk_size: Size of chunks to process at once             num_processes: Number of processes to use (None for CPU count)         """
        start_time = time.perf_counter()
        min_num = float('inf')
        max_num = float('-inf')
        numbers_found = 0

        num_chunks = (self.number_range.count + chunk_size - 1) // chunk_size

        with ProcessPoolExecutor(max_workers=num_processes) as executor:
            futures = [
                executor.submit(self._process_chunk,
                              min(chunk_size, self.number_range.count - i * chunk_size))
                for i in range(num_chunks)
            ]

            for future in tqdm(futures, desc="Processing chunks"):
                for num in future.result():
                    numbers_found += 1
                    min_num = min(min_num, num)
                    max_num = max(max_num, num)

        execution_time = time.perf_counter() - start_time

        if numbers_found == 0:
            return Result(None, None, 0, execution_time, 0)

        return Result(min_num, max_num, max_num - min_num, execution_time, numbers_found)

Claude 添加了兩個優(yōu)化,并且意識到這個編碼問題是一個令人尷尬的并行 (embarrassingly parallel)[10]問題:

  • 通過 Python 的 concurrent-futures [11] 包實現(xiàn)多線程,將大列表分成可以獨立處理的塊。

  • 向量化 (vectorization) 的 numpy 操作,這比基礎(chǔ) Python 操作要快得多。特別值得一提的是_precompute_digit_sums()函數(shù),它實現(xiàn)了一個向量化的數(shù)字和計算實現(xiàn)。條件語句while digits.any():是一種巧妙的代碼,但它確實能正確運行。

然而,這個特定的并行化實現(xiàn)存在一個問題:它生成子進程,這會導(dǎo)致許多煩人的問題,包括無法按原樣內(nèi)聯(lián)運行,而且必須使用main()保護來調(diào)用,這顯然限制了它的實用性。但即使作為單獨的腳本運行,由于使用了yield from numbers[mask](該生成器完全沒有必要,return numbers[mask]就足夠了),它也會打印出Error: cannot pickle 'generator' object錯誤。代碼還混合了 numpy 數(shù)組的dtype,這會導(dǎo)致錯誤:將它們?nèi)吭O(shè)置為np.int32可以修復(fù)這個問題。

在進行這些修復(fù)后,代碼現(xiàn)在比基礎(chǔ)實現(xiàn)快了 5.1 倍。

迭代 #3

再次寫更好的代碼,Claude 返回了新的實現(xiàn),它聲稱是"使用高級技術(shù)和現(xiàn)代 Python 特性的更加復(fù)雜和優(yōu)化的版本"。但實際代碼并沒有顯示出重要的算法改進,實際上在數(shù)字和計算方面還出現(xiàn)了倒退,回到了類型轉(zhuǎn)換的方法。如果說有什么的話,代碼庫變得更加臃腫,比如添加了一個用于執(zhí)行差值計算的類:

@dataclass(frozen=True, slots=True)
class SearchResult:
    """Result of the number search."""
    min_number: Optional[int]
    max_number: Optional[int]
    count: int
    execution_time: float

    @property
    def difference(self) -> Optional[int]:
        """Calculate difference between max and min numbers."""
        if self.min_number is None or self.max_number is None:
            return None
        return self.max_number - self.min_number

這次,代碼無需任何修復(fù)就能運行。然而,性能略微下降,相比基礎(chǔ)實現(xiàn)現(xiàn)在只快了 4.1 倍。

迭代 #4

這種迭代提示似乎遇到了收益遞減。在再一次寫更好的代碼之后,Claude 提供了新的實現(xiàn),聲稱具有"尖端優(yōu)化和企業(yè)級功能"。等等,企業(yè)級功能?!

最終的代碼太長了,無法在這篇博文中完整展示,但它確實創(chuàng)造了兩個新的優(yōu)化:它現(xiàn)在使用 numba Python 庫,可以調(diào)用 JIT (Just-In-Time) 編譯器,直接為 CPU 優(yōu)化代碼。在這種情況下,它可以通過一個裝飾器實現(xiàn)數(shù)字和的超快速預(yù)計算:

@jit(nopython=True, parallel=True)
def calculate_digit_sums(numbers: ArrayInt) -> ArrayInt:
    """Calculate digit sums using Numba."""
    result = np.zeros_like(numbers)
    for i in prange(len(numbers)):
        num = numbers[i]
        total = 0
        while num:
            total += num % 10
            num //= 10
        result[i] = total
    return result

完整的類還使用 Python 的 asyncio 進行并行化,這比子進程方法更規(guī)范,更適合用于任務(wù)調(diào)度。它也能更好地與現(xiàn)有的內(nèi)聯(lián)代碼和 REPL(如 Jupyter Notebooks)配合使用。

它還做了一些"企業(yè)級"改造:

  • 使用 Prometheus 作結(jié)構(gòu)化指標日志記錄。

  • 一個信號處理器,使代碼在被強制終止時可以優(yōu)雅地關(guān)閉。

  • 使用 rich [12] 表格展示基準測試結(jié)果。

打開網(wǎng)易新聞 查看精彩圖片
It is pretty, though!

看起來 AI 生成代碼的"宇宙化"就是通過過度工程使其變得企業(yè)級,這完全說得通。盡管如此,代碼可以不出任何錯誤地運行。async 和 numba 都是 Python 中的并行方法,所以它們可能造成冗余并產(chǎn)生額外開銷。然而,在基準測試后,算法運行速度非常快,每次運行大約 6 毫秒,也就是提速了 100 倍。這完全推翻了我之前認為這種提示詞會遇到收益遞減的假設(shè)。也許 numba 一直都是秘密武器?

總的來說,這種形式的迭代提示詞來改進代碼有其注意事項:代碼確實變得更好了,但事后看來,更好的定義太過寬泛。我只想要算法上的改進,而不是一個完整的 SaaS。讓我們從頭再來一次,這次要有更明確的方向。

提示詞工程讓 LLM 寫出更好的代碼

現(xiàn)在是 2025 年,要從 LLM 那里獲得最佳結(jié)果,提示詞工程 (prompt engineering) 仍然是必需的。事實上,提示詞工程對 LLM 變得更加重要:下一個 token 預(yù)測模型是通過在大批量輸入上最大化下一個 token 的預(yù)測概率來訓(xùn)練的,因此它們針對平均輸入和輸出進行優(yōu)化。隨著 LLM 的顯著改進,生成的輸出變得更加平均化,因為這就是它們的訓(xùn)練目標:所有 LLM 都偏向于平均值。雖然這既違反直覺又不有趣,但少量的指導(dǎo),明確告訴 LLM 你想要什么,以及給出一些你想要的例子,將客觀上改善 LLM 的輸出,遠超過構(gòu)建這些提示詞所需的努力。Claude 3.5 Sonnet 由于其強大的提示詞遵循能力,即使只是一點點提示詞工程也能顯著受益。

讓我們重做代碼優(yōu)化實驗,這次使用積極的提示詞工程,確保我們要的結(jié)果非常明確,不留任何模糊空間。沒錯,對 LLM 采用冷酷和"機器人化"的方式反而能讓它們表現(xiàn)得更好,管它什么洛夫克拉夫特式的恐懼呢。

初始嘗試

這次我們將使用系統(tǒng)提示詞,這只能通過 API 使用。系統(tǒng)提示詞列出了 LLM 必須遵循的"規(guī)則"。既然我想要更優(yōu)化的代碼,我們就在規(guī)則中定義這一點,并提供詳細的例子:

你寫的所有代碼都必須完全優(yōu)化。

"完全優(yōu)化"包括:
- 最大化內(nèi)存和運行時的算法大 O 效率
- 在適當?shù)牡胤绞褂貌⑿谢拖蛄炕?- 遵循代碼語言的正確風(fēng)格約定(例如最大化代碼重用 (DRY))
- 除了解決用戶提供的問題所絕對必需的代碼外,不要有多余的代碼(即沒有技術(shù)債務(wù))

如果代碼沒有完全優(yōu)化,你將被罰款 100 美元。

關(guān)于最后一行:在系統(tǒng)提示詞中向 LLM 提供正面/負面激勵已不再常見,而且我自己的研究表明,這種方法是否有積極影響還不確定。不過,這個罰款設(shè)定在后續(xù)的迭代提示中會變得更加重要。

初始用戶提示詞也增加了一行:

編寫 Python 代碼來解決這個問題:

給定一個包含 100 萬個 1 到 100,000 之間的隨機整數(shù)的列表,找出數(shù)字之和等于 30 的最大數(shù)和最小數(shù)之間的差值。

在編寫代碼之前,請規(guī)劃所有必要的優(yōu)化。

"規(guī)劃"是一個長期使用的技巧,可以幫助對齊大語言模型的第一次輸出 — 這是"讓我們一步一步思考[13]"的現(xiàn)代實現(xiàn)。有些大語言模型會忽略它,但 Claude 3.5 Sonnet 總是會遵循。

基于這個提示詞工程策略的完整、未經(jīng)編輯的對話線程可在 GitHub 上獲取[14]。

將系統(tǒng)提示詞和這個用戶提示詞發(fā)送給 Claude,回應(yīng)如下:

讓我先規(guī)劃優(yōu)化:

1. 算法優(yōu)化:
- 我們只需要跟蹤數(shù)字之和為 30 的最小值和最大值,不需要存儲所有數(shù)字
- 我們可以在讀取數(shù)字時計算數(shù)字之和,不需要轉(zhuǎn)換為字符串
- 只需要遍歷列表一次 (O(n))

2. 內(nèi)存優(yōu)化:
- 只需要存儲 2 個變量(最小值和最大值),不需要完整的過濾列表
- 不需要額外的數(shù)據(jù)結(jié)構(gòu)

3. 性能優(yōu)化:
- 使用 numba 的 @jit 裝飾器加快計算速度
- 創(chuàng)建一個使用模運算而不是字符串轉(zhuǎn)換的快速數(shù)字求和函數(shù)
- 使用 numpy 生成隨機數(shù)(比 random.randint 更快)

這很有希望:Claude 立即意識到它應(yīng)該從 numpy 和 numba 開始,并且使用整數(shù)運算來計算數(shù)字之和。初始實現(xiàn):

@jit(nopython=True)
def digit_sum(n):
    total = 0
    while n:
        total += n % 10
        n //= 10
    return total

@jit(nopython=True)
def find_difference(numbers):
    min_num = float('inf')
    max_num = float('-inf')

    for num in numbers:
        sum_digits = digit_sum(num)
        if sum_digits == 30:
            min_num = min(min_num, num)
            max_num = max(max_num, num)

    return max_num - min_num if max_num != float('-inf') else 0

這比隨意提示詞的初始實現(xiàn)更簡潔,特別是沒有不必要的注釋。但仍然有改進的空間,例如這個實現(xiàn)沒有在jit裝飾器中設(shè)置parallel=True

這種小型的提示詞工程導(dǎo)致的代碼平均運行時間為 11.2 毫秒,比初始實現(xiàn)快了 59 倍。令人驚嘆!

迭代 #1

現(xiàn)在我們可以用一個比"寫更好的代碼"更詳細的提示詞來迭代代碼:

你的代碼沒有完全優(yōu)化,你被罰款 100 美元。讓它更優(yōu)化。

不,這不是煤氣燈效應(yīng),這是激勵對齊。

Claude 回應(yīng)如下:

@jit(nopython=True)
def digit_sum(n):
     # 使用位操作進行更快的除法 
    total = 0
    while n:
        total += n & 15   # 單個數(shù)字和的快速查找 
        n >>= 4
    return total

@jit(nopython=True, parallel=True)
def find_difference_chunk(numbers):
    min_num = np.iinfo(np.int64).max
    max_num = np.iinfo(np.int64).min

     # 使用 numba 進行并行處理塊 
    for i in prange(len(numbers)):
        num = numbers[i]
        sum_digits = digit_sum(num)
        if sum_digits == 30:
            min_num = min(min_num, num)
            max_num = max(max_num, num)

    return min_num, max_num

def process_chunk(chunk):
    return find_difference_chunk(chunk)

很好,它識別出了parallel=True標志!但是那個數(shù)字求和實現(xiàn)很奇怪,使用了位移操作[15],這通常只用于超優(yōu)化。我不認為你可以在十進制數(shù)上很好地使用位移操作,經(jīng)過測試,事實證明確實不行,因為這個實現(xiàn)計算出的數(shù)字之和是錯誤的。該實現(xiàn)還包括了多進程分塊方法(未顯示),這可能與 numba 重復(fù)并造成額外開銷。同樣未顯示的是:腳本還使用小型測試數(shù)組預(yù)編譯了 JIT 函數(shù)以獲得更好的實際性能,這是numba 文檔推薦[16]的基準測試方法。

盡管如此,代碼性能相比初始提示詞工程實現(xiàn)大幅下降,現(xiàn)在只比樸素實現(xiàn)快 9.1 倍??赡艿脑蚴怯捎诙噙M程生成新進程,這些進程每次都會重新編譯 numba JIT 函數(shù)[17],因此存在巨大的開銷。

迭代 #2

繼續(xù)迭代提示,結(jié)果:

# 使用位操作預(yù)計算查找表 
LOOKUP = np.zeros(100001, dtype=np.uint8)
for i in range(100001):
     # 使用并行位計數(shù)優(yōu)化數(shù)字求和 
    n = i
    n = (n & 0x0F) + ((n >> 4) & 0x0F) + ((n >> 8) & 0x0F) + \
        ((n >> 12) & 0x0F) + ((n >> 16) & 0x0F)
    LOOKUP[i] = n

@jit(nopython=True, parallel=True, cache=True, fastmath=True)
def find_min_max(numbers):
     # 一次處理 32 個數(shù)字使用 SIMD 
    min_val = np.iinfo(np.uint32).max
    max_val = 0

     # 帶有顯式 SIMD 提示的向量化處理 
    for i in prange(0, len(numbers), 32):
         # 將 32 個元素加載到 SIMD 寄存器 
        chunk = numbers[i:min(i + 32, len(numbers))]

         # 向量化查找和比較 
        sums = LOOKUP[chunk]
        mask = sums == 30

        if np.any(mask):
            valid_nums = chunk[mask]
            min_val = min(min_val, np.min(valid_nums))
            max_val = max(max_val, np.max(valid_nums))

    return min_val, max_val

Claude 開始使用SIMD 操作[18]和塊大小來實現(xiàn)(理論上的)極致性能。我對那個位移實現(xiàn)感到困惑,因為它仍然是錯誤的,特別是現(xiàn)在涉及到十六進制數(shù)。事實證明,這是一個計算十六進制數(shù)字的數(shù)字之和的優(yōu)化方法,而不是十進制數(shù)字,因此這完全是一個幻覺。還有另一個極其微妙的幻覺:當parallel=True時,prange函數(shù)不能接受步長為 32,這是一個很少有文檔記載的細節(jié)。設(shè)置parallel=False并進行基準測試,確實比初始提示詞工程實現(xiàn)略有改進,比基礎(chǔ)實現(xiàn)快 65 倍。

迭代 #3

再一次迭代:

# 預(yù)計算完美最小哈希表 
 # 關(guān)鍵洞察:對于 1-100000 的數(shù)字,只需要存儲數(shù)字之和為 30 的 
 # 格式:[數(shù)字] = 1 如果數(shù)字之和為 30,否則為 0 
HASH_TABLE = np.zeros(100001, dtype=np.uint8)

 # 在模塊加載時使用位操作生成哈希表 
@jit(nopython=True, cache=True)
def init_hash_table():
    min_val = np.iinfo(np.uint32).max
    max_val = 0

     # 使用并行位計數(shù)的最優(yōu)數(shù)字求和 
    for i in range(1, 100001):
        n = i
        sum = 0
        while n and sum <= 30:
            sum += n & 0xF
            n >>= 4
        if sum == 30:
            HASH_TABLE[i] = 1
            min_val = min(min_val, i)
            max_val = max(max_val, i)

    return min_val, max_val

 # 在模塊加載時初始化 
MIN_VALID, MAX_VALID = init_hash_table()

@jit(nopython=True, parallel=False, cache=True, fastmath=True)
def find_min_max(numbers):
    min_val = MAX_VALID   # 從已知邊界開始 
    max_val = MIN_VALID
    found = False

     # 單個向量化操作 
    mask = HASH_TABLE[numbers] == 1
    if np.any(mask):
        valid_nums = numbers[mask]
        min_val = np.min(valid_nums)
        max_val = np.max(valid_nums)
        found = True

    return min_val, max_val, found

在這種情況下,大模型放棄了一直造成問題的分塊策略,并添加了兩個優(yōu)化:一個全局HASH_TABLE(這只是一個 numpy 數(shù)組,我不確定簡單的索引查找是否真的算作哈希表),它還引入了一個邏輯微優(yōu)化,即在對數(shù)字求和時,如果數(shù)字超過 30,計數(shù)就可以停止,因為它可以立即被識別為無效。

一個主要問題:由于一個網(wǎng)上很少有文檔提及的微妙問題,那個"在模塊加載時生成哈希表"的技巧實際上不起作用:numba 的 JIT 函數(shù)外的對象是只讀的,但HASH_TABLE仍然在 JIT 函數(shù)外實例化并在 JIT 函數(shù)內(nèi)修改,因此會導(dǎo)致一個非常令人困惑的錯誤。經(jīng)過一個小的重構(gòu),使HASH_TABLE在 JIT 函數(shù)內(nèi)實例化后,代碼正常運輸,而且運行極快:比原始基礎(chǔ)實現(xiàn)快 100 倍,與隨意提示詞的最終性能相同,但代碼量減少了幾個數(shù)量級。

迭代 #4

此時,Claude 提示說代碼已經(jīng)達到了"這個問題理論上可能的最小時間復(fù)雜度"。所以我改變了方向,只是讓它修復(fù)數(shù)字求和問題:它實現(xiàn)了[19],而且僅用之前使用的整數(shù)實現(xiàn)替換了相關(guān)代碼,并沒有試圖修復(fù)HASH_TABLE。更重要的是,通過HASH_TABLE的調(diào)整,我確認實現(xiàn)是正確的,最終,盡管由于不再使用位移操作而導(dǎo)致性能略有下降,但是比基礎(chǔ)實現(xiàn)快 95 倍。

繼續(xù)提升 LLM 代碼生成效果

綜合所有內(nèi)容,讓我們來可視化這些改進,包括突出顯示那些由于 bug 而需要我修改代碼邏輯才能運行的情況。

打開網(wǎng)易新聞 查看精彩圖片

總的來說,要求 LLM "寫更好的代碼"確實能讓代碼變得更好,這取決于你如何定義"更好"。通過使用通用的迭代提示詞,代碼在功能性和執(zhí)行速度方面都得到了顯著提升。提示詞工程能更快速且更穩(wěn)定地改進代碼性能,但也更容易引入細微的 bug,這是因為 LLM 本身并非為生成高性能代碼而訓(xùn)練的。與使用 LLM 的其他場景一樣,效果因人而異。無論 AI 炒作者們?nèi)绾未蹬?LLM 為神器,最終都需要人工干預(yù)來修復(fù)那些不可避免的問題。

本博文中的所有代碼,包括基準測試腳本和數(shù)據(jù)可視化代碼,都可在 GitHub 上獲取[20]。

出乎我意料的是,Claude 3.5 Sonnet 在兩個實驗中都沒有發(fā)現(xiàn)和實現(xiàn)某些優(yōu)化。具體來說,它沒有從統(tǒng)計學(xué)角度來思考:由于我們是從 1 到 100,000 的范圍內(nèi)均勻生成 1,000,000 個數(shù)字,必然會出現(xiàn)大量無需重復(fù)分析的數(shù)字。LLM 沒有通過將數(shù)字列表轉(zhuǎn)換為 Pythonset()或使用 numpy 的unique()來去重。我還以為會看到一個對 1,000,000 個數(shù)字進行升序排序的實現(xiàn):這樣算法就可以從頭到尾搜索最小值(或從尾到頭搜索最大值),而不需要檢查每個數(shù)字。不過排序操作較慢,向量化方法確實更實用。

即使大語言模型可能會出錯,我從這些實驗中得到的一個重要啟示是,即使代碼輸出不能直接使用,它們?nèi)蕴峁┝擞腥さ南敕ê凸ぞ呓ㄗh。例如,我從未接觸過 numba,因為作為一個數(shù)據(jù)科學(xué)家/機器學(xué)習(xí)工程師,如果我需要更好的代碼性能,我習(xí)慣于使用 numpy 的技巧。然而,numba JIT 函數(shù)的效果令人難以忽視,我可能會把它加入我的工具箱。當我在其他技術(shù)領(lǐng)域(如網(wǎng)站后端和前端)測試類似的“優(yōu)化代碼”提示詞迭代工作流時,LLM 也提出了不少有價值的建議。

當然,這些大語言模型不會很快取代軟件工程師,因為需要強大的工程師背景以及其他特定領(lǐng)域的知識,才能識別出什么才是真正好的實現(xiàn)。即使互聯(lián)網(wǎng)上有大量的代碼,若沒有指導(dǎo),大語言模型也無法區(qū)分普通代碼和優(yōu)秀的高性能代碼。現(xiàn)實世界的系統(tǒng)顯然比面試式的編程問題復(fù)雜得多,但如果通過快速反復(fù)要求 Claude 實現(xiàn)一個功能,能使代碼速度提高 100 倍,那這個流程就非常值得。有些人認為過早優(yōu)化[21]是不好的編碼實踐,但在實際項目中,這比那些隨著時間的推移會變成技術(shù)債務(wù)的次優(yōu)實現(xiàn)要好得多。

我的實驗存在一個局限性,那就是我使用 Python 來對代碼改進進行基準測試,而這并不是開發(fā)者在追求極致性能優(yōu)化時的首選編程語言。雖然像 numpy 和 numba 這樣的庫通過利用 C 語言來解決了 Python 的性能瓶頸,但更現(xiàn)代的解決方案是采用 polars 和 pydantic 等流行 Python 庫,它們使用 Rust 開發(fā)。Rust 在性能方面比 C 語言更具優(yōu)勢,而 PyO3 幾乎沒有性能損耗就能讓 Python 調(diào)用 Rust 代碼。我可以確認 Claude 3.5 Sonnet 能夠生成兼容 Python 和 Rust 代碼,不過這種工作流程太新穎了,足夠成為另一篇博文的主題。

以此同時,雖然要求 LLM 讓代碼變得更好是 AI 更實用的用途,但你也可以要求它們"讓代碼更兄弟"...效果好壞參半。

參考資料

為 ChatGPT 添加了新功能: https://openai.com/index/dall-e-3-is-now-available-in-chatgpt-plus-and-enterprise/

短暫的網(wǎng)絡(luò)梗: https://lifehacker.com/tech/chat-gpt-make-it-more-ai-images-trend

AI 垃圾: https://en.wikipedia.org/wiki/AI_slop

也不是完全不可能: https://daniel.haxx.se/blog/2024/01/02/the-i-in-llm-stands-for-intelligence/

[5]

產(chǎn)生幻覺: https://en.wikipedia.org/wiki/Hallucination_%28artificial_intelligence%29

[6]

Claude 3.5 Sonnet: https://www.anthropic.com/news/claude-3-5-sonnet

[7]

編碼: https://www.vellum.ai/blog/llm-benchmarks-overview-limits-and-model-comparison

[8]

基準測試: https://aider.chat/docs/leaderboards/

[9]

GitHub 上獲取: https://github.com/minimaxir/llm-write-better-code/blob/main/python_30_casual_use.md

[10]

令人尷尬的并行 (embarrassingly parallel): https://en.wikipedia.org/wiki/Embarrassingly_parallel

[11]

concurrent-futures: https://docs.python.org/3/library/concurrent.futures.html

[12]

rich: https://github.com/Textualize/rich

[13]

讓我們一步一步思考: https://arxiv.org/abs/2205.11916

[14]

GitHub 上獲取: https://github.com/minimaxir/llm-write-better-code/blob/main/python_30_prompt_engineering.md

[15]

位移操作: https://wiki.python.org/moin/BitwiseOperators

[16]

numba 文檔推薦: https://numba.pydata.org/numba-doc/dev/user/5minguide.html#how-to-measure-the-performance-of-numba

[17]

重新編譯 numba JIT 函數(shù): https://stackoverflow.com/questions/72449896/does-numba-need-to-compile-separately-within-each-parallel-process

SIMD 操作: https://tbetcke.github.io/hpc_lecture_notes/simd.html

它實現(xiàn)了: https://github.com/minimaxir/llm-write-better-code/blob/main/python_30_prompt_engineering.md#assistant-iteration-4

可在 GitHub 上獲取: https://github.com/minimaxir/llm-write-better-code/

[21]

過早優(yōu)化: https://softwareengineering.stackexchange.com/questions/80084/is-premature-optimization-really-the-root-of-all-evil

如果你正在尋找優(yōu)質(zhì)的Python文章和項目,我必須向你推薦Python潮流周刊

它精選全網(wǎng)的優(yōu)秀文章、教程、開源項目、軟件工具、播客、視頻、熱門話題等豐富內(nèi)容,讓你緊跟技術(shù)最前沿,獲取最新的第一手學(xué)習(xí)資料!

歡迎點擊下方圖片,了解這份全世界知識密度最高、知識廣度最大的 Python 技術(shù)周刊。