<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Memory on Tarragon</title><link>https://tarrragon.github.io/blog/tags/memory/</link><description>Recent content in Memory on Tarragon</description><generator>Hugo -- gohugo.io</generator><language>zh-TW</language><copyright>Tarragon (CC BY 4.0)</copyright><lastBuildDate>Tue, 16 Jun 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://tarrragon.github.io/blog/tags/memory/index.xml" rel="self" type="application/rss+xml"/><item><title>Agent Memory</title><link>https://tarrragon.github.io/blog/llm/knowledge-cards/agent-memory/</link><pubDate>Tue, 12 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/llm/knowledge-cards/agent-memory/</guid><description>&lt;p>Agent memory 的核心概念是「&lt;strong>agent 在 &lt;a href="https://tarrragon.github.io/blog/llm/knowledge-cards/context-window/" data-link-title="Context Window" data-link-desc="模型一次能處理的最大 token 數量：prompt 加生成的總和上限">context window&lt;/a> 之外管理長期狀態的設計&lt;/strong>」、把使用者偏好、過去任務、知識、操作流程等持久化、跨 session 重用。借鑒人類認知科學的五個層次：working memory（context 本身）、short-term（session scratchpad）、long-term episodic（過去事件）、long-term semantic（事實 / 知識）、long-term procedural（流程 / 技能）。&lt;/p>
&lt;h2 id="概念位置">概念位置&lt;/h2>
&lt;p>五個層次的對比：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>層&lt;/th>
 &lt;th>範圍&lt;/th>
 &lt;th>存放位置&lt;/th>
 &lt;th>典型內容&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Working memory&lt;/td>
 &lt;td>當前 query / forward pass&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/llm/knowledge-cards/context-window/" data-link-title="Context Window" data-link-desc="模型一次能處理的最大 token 數量：prompt 加生成的總和上限">Context window&lt;/a> 本身&lt;/td>
 &lt;td>當下對話、tool result、reasoning trace&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Short-term / session memory&lt;/td>
 &lt;td>單一 session（小時級）&lt;/td>
 &lt;td>Scratchpad 物件 / &lt;a href="https://tarrragon.github.io/blog/llm/knowledge-cards/prompt-cache/" data-link-title="Prompt Cache" data-link-desc="重複出現的 prompt prefix 在推論伺服器或 LLM 服務端被 cache、後續 query 跳過 prefill、大幅降 cost 跟 TTFT">prompt cache&lt;/a>&lt;/td>
 &lt;td>Session 內累積的中間結果、用過的策略&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Long-term episodic memory&lt;/td>
 &lt;td>跨 session（永久）&lt;/td>
 &lt;td>DB / vector store / file system&lt;/td>
 &lt;td>「上週 alice 問過 X」「上個 sprint 解過 Y bug」&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Long-term semantic memory&lt;/td>
 &lt;td>跨 session（永久）&lt;/td>
 &lt;td>DB / vector store / KG&lt;/td>
 &lt;td>「user 偏好 markdown 輸出」「專案用 React 18」「Python 3.11」&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Long-term procedural memory&lt;/td>
 &lt;td>跨 session（永久）&lt;/td>
 &lt;td>Skill registry / playbook&lt;/td>
 &lt;td>「跑測試前先 npm install」「commit 前要 lint」&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>跟其他相關概念的關係：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>概念&lt;/th>
 &lt;th>跟 agent memory 的關係&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/llm/knowledge-cards/rag/" data-link-title="RAG" data-link-desc="Retrieval-Augmented Generation：動態外掛知識給 LLM、繞開模型參數記憶的靜態限制">RAG&lt;/a>&lt;/td>
 &lt;td>Long-term semantic memory 的常見實作（vector store retrieval）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/llm/knowledge-cards/context-window/" data-link-title="Context Window" data-link-desc="模型一次能處理的最大 token 數量：prompt 加生成的總和上限">Context window&lt;/a>&lt;/td>
 &lt;td>Working memory 的物理上限&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/llm/knowledge-cards/system-prompt/" data-link-title="System Prompt" data-link-desc="LLM application 中由開發者預設、不直接顯示給使用者的指令層、定義模型的角色、行為規範、輸出格式">System prompt&lt;/a>&lt;/td>
 &lt;td>把 semantic / procedural memory 編碼進 scaffold 的方式&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/llm/knowledge-cards/subagent/" data-link-title="Subagent" data-link-desc="Coding agent 中把特定責任拆給專門子 agent 的設計模式、各 subagent 有獨立 context、由 main agent 透過 handoff 調度">Subagent&lt;/a>&lt;/td>
 &lt;td>用 subagent 分隔不同 specialty 的 memory&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="設計責任">設計責任&lt;/h2>
&lt;p>讀 agent paper / 設計 / framework docs 看到「agent memory」「memory store」「mem0 / Letta」「episodic / semantic memory」就是這 framing。寫 code 場景的判讀：&lt;/p></description><content:encoded><![CDATA[<p>Agent memory 的核心概念是「<strong>agent 在 <a href="/blog/llm/knowledge-cards/context-window/" data-link-title="Context Window" data-link-desc="模型一次能處理的最大 token 數量：prompt 加生成的總和上限">context window</a> 之外管理長期狀態的設計</strong>」、把使用者偏好、過去任務、知識、操作流程等持久化、跨 session 重用。借鑒人類認知科學的五個層次：working memory（context 本身）、short-term（session scratchpad）、long-term episodic（過去事件）、long-term semantic（事實 / 知識）、long-term procedural（流程 / 技能）。</p>
<h2 id="概念位置">概念位置</h2>
<p>五個層次的對比：</p>
<table>
  <thead>
      <tr>
          <th>層</th>
          <th>範圍</th>
          <th>存放位置</th>
          <th>典型內容</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Working memory</td>
          <td>當前 query / forward pass</td>
          <td><a href="/blog/llm/knowledge-cards/context-window/" data-link-title="Context Window" data-link-desc="模型一次能處理的最大 token 數量：prompt 加生成的總和上限">Context window</a> 本身</td>
          <td>當下對話、tool result、reasoning trace</td>
      </tr>
      <tr>
          <td>Short-term / session memory</td>
          <td>單一 session（小時級）</td>
          <td>Scratchpad 物件 / <a href="/blog/llm/knowledge-cards/prompt-cache/" data-link-title="Prompt Cache" data-link-desc="重複出現的 prompt prefix 在推論伺服器或 LLM 服務端被 cache、後續 query 跳過 prefill、大幅降 cost 跟 TTFT">prompt cache</a></td>
          <td>Session 內累積的中間結果、用過的策略</td>
      </tr>
      <tr>
          <td>Long-term episodic memory</td>
          <td>跨 session（永久）</td>
          <td>DB / vector store / file system</td>
          <td>「上週 alice 問過 X」「上個 sprint 解過 Y bug」</td>
      </tr>
      <tr>
          <td>Long-term semantic memory</td>
          <td>跨 session（永久）</td>
          <td>DB / vector store / KG</td>
          <td>「user 偏好 markdown 輸出」「專案用 React 18」「Python 3.11」</td>
      </tr>
      <tr>
          <td>Long-term procedural memory</td>
          <td>跨 session（永久）</td>
          <td>Skill registry / playbook</td>
          <td>「跑測試前先 npm install」「commit 前要 lint」</td>
      </tr>
  </tbody>
</table>
<p>跟其他相關概念的關係：</p>
<table>
  <thead>
      <tr>
          <th>概念</th>
          <th>跟 agent memory 的關係</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/llm/knowledge-cards/rag/" data-link-title="RAG" data-link-desc="Retrieval-Augmented Generation：動態外掛知識給 LLM、繞開模型參數記憶的靜態限制">RAG</a></td>
          <td>Long-term semantic memory 的常見實作（vector store retrieval）</td>
      </tr>
      <tr>
          <td><a href="/blog/llm/knowledge-cards/context-window/" data-link-title="Context Window" data-link-desc="模型一次能處理的最大 token 數量：prompt 加生成的總和上限">Context window</a></td>
          <td>Working memory 的物理上限</td>
      </tr>
      <tr>
          <td><a href="/blog/llm/knowledge-cards/system-prompt/" data-link-title="System Prompt" data-link-desc="LLM application 中由開發者預設、不直接顯示給使用者的指令層、定義模型的角色、行為規範、輸出格式">System prompt</a></td>
          <td>把 semantic / procedural memory 編碼進 scaffold 的方式</td>
      </tr>
      <tr>
          <td><a href="/blog/llm/knowledge-cards/subagent/" data-link-title="Subagent" data-link-desc="Coding agent 中把特定責任拆給專門子 agent 的設計模式、各 subagent 有獨立 context、由 main agent 透過 handoff 調度">Subagent</a></td>
          <td>用 subagent 分隔不同 specialty 的 memory</td>
      </tr>
  </tbody>
</table>
<h2 id="設計責任">設計責任</h2>
<p>讀 agent paper / 設計 / framework docs 看到「agent memory」「memory store」「mem0 / Letta」「episodic / semantic memory」就是這 framing。寫 code 場景的判讀：</p>
<ol>
<li><strong>不是每個 agent 都需要五個層次都用</strong>：autocomplete 只要 working memory；對話 IDE assistant 多用 working + session；長期 coding agent 才需要 long-term</li>
<li><strong>Long-term memory 的兩條實作路線</strong>：(a) retrieval-on-demand（vector store + similarity search、見 <a href="/blog/llm/knowledge-cards/rag/" data-link-title="RAG" data-link-desc="Retrieval-Augmented Generation：動態外掛知識給 LLM、繞開模型參數記憶的靜態限制">RAG</a>）、(b) injection-on-startup（把關鍵 memory 編進 system prompt、適合小量穩定的 procedural）</li>
<li><strong>失敗模式</strong>：memory drift（舊 memory 過時但仍被 retrieve）、PII 寫入（user 不知情下被存）、context 污染（不相關 memory 被 inject 進 working）、跟 <a href="/blog/llm/knowledge-cards/hallucination/" data-link-title="Hallucination" data-link-desc="LLM 生成內容看起來合理但事實錯誤、引用不存在的來源、虛構不存在的 entity 的現象">hallucination</a> 互相 boost</li>
<li><strong>跟 <a href="/blog/llm/04-applications/agent-memory-architecture/" data-link-title="4.19 Agent memory 分層架構" data-link-desc="Agent 在 context window 之外管理長期狀態的設計：working / short-term / long-term episodic / semantic / procedural 五個層次、寫入時機、retrieval 設計、失敗模式">4.19 agent memory 章節</a> 的關係</strong>：本卡是分類定義、章節是工程實務（寫入時機、retrieval 設計、失敗模式緩解）</li>
</ol>
]]></content:encoded></item><item><title>MoE CPU 卸載</title><link>https://tarrragon.github.io/blog/llm/knowledge-cards/moe-cpu-offload/</link><pubDate>Tue, 12 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/llm/knowledge-cards/moe-cpu-offload/</guid><description>&lt;p>MoE CPU 卸載的核心概念是「&lt;a href="https://en.wikipedia.org/wiki/Mixture_of_experts">Mixture-of-Experts&lt;/a> 模型每個 token 只啟用少數專家、把不活躍的專家權重留在系統 RAM、用到再走 PCIe 拉回 GPU」。它讓 16GB VRAM 卡能載入 30B / 70B 等級的 MoE 模型、是 &lt;a href="https://tarrragon.github.io/blog/llm/05-discrete-gpu/" data-link-title="模組五：Windows / Linux &amp;#43; 獨立 GPU" data-link-desc="消費級 PC（Windows / Linux &amp;#43; NVIDIA / AMD 獨立 GPU）跑本地 LLM 的硬體判讀、MoE CPU 卸載、KV cache 量化與 llama.cpp 調參">獨立 GPU 場景&lt;/a> 相對 &lt;a href="https://tarrragon.github.io/blog/llm/knowledge-cards/unified-memory/" data-link-title="Unified Memory Architecture" data-link-desc="Apple Silicon 讓 CPU / GPU / NE 共用同一塊記憶體：跑大模型的優勢來源">統一記憶體&lt;/a> 場景多出的工程選項。&lt;/p>
&lt;h2 id="概念位置">概念位置&lt;/h2>
&lt;p>MoE 卸載屬於「推論時的權重位置管理」、跟 &lt;a href="https://tarrragon.github.io/blog/llm/knowledge-cards/quantization/" data-link-title="Quantization" data-link-desc="用較少 bits 表示模型權重：壓縮記憶體佔用、加快生字速度，代價是少量品質衰減">量化&lt;/a> 屬於「權重精度壓縮」是兩個獨立維度、可以疊加（如 30B MoE Q4 + 卸載部分層、模型精度跟記憶體位置同時被處理）。它跟 &lt;a href="https://tarrragon.github.io/blog/llm/knowledge-cards/kv-cache/" data-link-title="KV Cache" data-link-desc="已處理 token 的 attention 中間結果暫存：避免重算、加速後續生成">KV cache&lt;/a> 量化是 PC 場景常一起使用的兩個工具：卸載騰出 VRAM、KV cache 量化讓騰出的 VRAM 拿去開大 &lt;a href="https://tarrragon.github.io/blog/llm/knowledge-cards/context-window/" data-link-title="Context Window" data-link-desc="模型一次能處理的最大 token 數量：prompt 加生成的總和上限">context window&lt;/a>。&lt;/p>
&lt;p>在 llama.cpp 中、對應的旗標是 &lt;code>--n-cpu-moe &amp;lt;N&amp;gt;&lt;/code>、把 N 層的 MoE 專家權重保留在 CPU 記憶體。例如 &lt;code>--n-cpu-moe 30&lt;/code> 表示 30 層的專家層留 RAM、其餘走 GPU。&lt;/p>
&lt;h2 id="可觀察訊號與例子">可觀察訊號與例子&lt;/h2>
&lt;p>以 Qwen3-30B-A3B Q4_K_M（模型體積 10 GB 級、active parameter 約 3B 等級）為例、不同卸載策略下記憶體分布與生字速度的相對方向（具體數值依驅動、CUDA backend、模型版本、PCIe 版本變化、本表用於說明趨勢、不是嚴格 benchmark）：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>配置&lt;/th>
 &lt;th>卸載策略&lt;/th>
 &lt;th>VRAM 佔用方向&lt;/th>
 &lt;th>RAM 佔用方向&lt;/th>
 &lt;th>生字速度方向（同卡比較）&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>全載 VRAM&lt;/td>
 &lt;td>&lt;code>--n-cpu-moe 0&lt;/code>&lt;/td>
 &lt;td>接近 VRAM 上限&lt;/td>
 &lt;td>系統正常&lt;/td>
 &lt;td>上限取決於 VRAM 頻寬&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>中度卸載&lt;/td>
 &lt;td>&lt;code>--n-cpu-moe ~20&lt;/code>&lt;/td>
 &lt;td>顯著下降&lt;/td>
 &lt;td>上升至 10 GB 級&lt;/td>
 &lt;td>較全載小幅下降&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>重度卸載&lt;/td>
 &lt;td>&lt;code>--n-cpu-moe ~30&lt;/code>&lt;/td>
 &lt;td>大幅下降&lt;/td>
 &lt;td>上升較多&lt;/td>
 &lt;td>較全載明顯下降&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>極限卸載&lt;/td>
 &lt;td>&lt;code>--n-cpu-moe ~40&lt;/code>&lt;/td>
 &lt;td>接近最低&lt;/td>
 &lt;td>上升最多&lt;/td>
 &lt;td>較全載大幅下降&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;blockquote>
&lt;p>&lt;strong>事實查核註&lt;/strong>：上表是趨勢示意、不是經本文系統實測的數值。實際數值依顯卡型號、PCIe 版本、CUDA backend、GGUF 量化版本、&lt;code>-ngl&lt;/code> 設定、context 長度與 batch size 變化、建議用 &lt;code>llama-bench&lt;/code> 或實際工作流校準。&lt;/p>&lt;/blockquote>
&lt;p>社群常見的觀察是：MoE 卸載對生字速度的衰減幅度、相對於「Dense 模型把同樣比例的層卸載到 CPU」較小、原因是 MoE 每 token 只啟用少數專家、PCIe 上的權重傳輸量也較少；具體幅度依模型架構（active parameter 比例、專家數）變化。&lt;/p>
&lt;h2 id="設計責任">設計責任&lt;/h2>
&lt;p>理解 MoE 卸載後、可以解釋三個 PC 場景的現象：16GB VRAM 卡能載入 30B 級 MoE 模型（透過部分卸載而非全載 VRAM）、PC 場景 64GB RAM 相對 32GB 在 MoE 卸載空間上明顯更寬裕（可卸載更多層）、Mac 統一記憶體場景較少需要「卸載」這個概念（VRAM 跟 RAM 共用、不需要在兩個區域之間搬資料）。&lt;/p></description><content:encoded><![CDATA[<p>MoE CPU 卸載的核心概念是「<a href="https://en.wikipedia.org/wiki/Mixture_of_experts">Mixture-of-Experts</a> 模型每個 token 只啟用少數專家、把不活躍的專家權重留在系統 RAM、用到再走 PCIe 拉回 GPU」。它讓 16GB VRAM 卡能載入 30B / 70B 等級的 MoE 模型、是 <a href="/blog/llm/05-discrete-gpu/" data-link-title="模組五：Windows / Linux &#43; 獨立 GPU" data-link-desc="消費級 PC（Windows / Linux &#43; NVIDIA / AMD 獨立 GPU）跑本地 LLM 的硬體判讀、MoE CPU 卸載、KV cache 量化與 llama.cpp 調參">獨立 GPU 場景</a> 相對 <a href="/blog/llm/knowledge-cards/unified-memory/" data-link-title="Unified Memory Architecture" data-link-desc="Apple Silicon 讓 CPU / GPU / NE 共用同一塊記憶體：跑大模型的優勢來源">統一記憶體</a> 場景多出的工程選項。</p>
<h2 id="概念位置">概念位置</h2>
<p>MoE 卸載屬於「推論時的權重位置管理」、跟 <a href="/blog/llm/knowledge-cards/quantization/" data-link-title="Quantization" data-link-desc="用較少 bits 表示模型權重：壓縮記憶體佔用、加快生字速度，代價是少量品質衰減">量化</a> 屬於「權重精度壓縮」是兩個獨立維度、可以疊加（如 30B MoE Q4 + 卸載部分層、模型精度跟記憶體位置同時被處理）。它跟 <a href="/blog/llm/knowledge-cards/kv-cache/" data-link-title="KV Cache" data-link-desc="已處理 token 的 attention 中間結果暫存：避免重算、加速後續生成">KV cache</a> 量化是 PC 場景常一起使用的兩個工具：卸載騰出 VRAM、KV cache 量化讓騰出的 VRAM 拿去開大 <a href="/blog/llm/knowledge-cards/context-window/" data-link-title="Context Window" data-link-desc="模型一次能處理的最大 token 數量：prompt 加生成的總和上限">context window</a>。</p>
<p>在 llama.cpp 中、對應的旗標是 <code>--n-cpu-moe &lt;N&gt;</code>、把 N 層的 MoE 專家權重保留在 CPU 記憶體。例如 <code>--n-cpu-moe 30</code> 表示 30 層的專家層留 RAM、其餘走 GPU。</p>
<h2 id="可觀察訊號與例子">可觀察訊號與例子</h2>
<p>以 Qwen3-30B-A3B Q4_K_M（模型體積 10 GB 級、active parameter 約 3B 等級）為例、不同卸載策略下記憶體分布與生字速度的相對方向（具體數值依驅動、CUDA backend、模型版本、PCIe 版本變化、本表用於說明趨勢、不是嚴格 benchmark）：</p>
<table>
  <thead>
      <tr>
          <th>配置</th>
          <th>卸載策略</th>
          <th>VRAM 佔用方向</th>
          <th>RAM 佔用方向</th>
          <th>生字速度方向（同卡比較）</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>全載 VRAM</td>
          <td><code>--n-cpu-moe 0</code></td>
          <td>接近 VRAM 上限</td>
          <td>系統正常</td>
          <td>上限取決於 VRAM 頻寬</td>
      </tr>
      <tr>
          <td>中度卸載</td>
          <td><code>--n-cpu-moe ~20</code></td>
          <td>顯著下降</td>
          <td>上升至 10 GB 級</td>
          <td>較全載小幅下降</td>
      </tr>
      <tr>
          <td>重度卸載</td>
          <td><code>--n-cpu-moe ~30</code></td>
          <td>大幅下降</td>
          <td>上升較多</td>
          <td>較全載明顯下降</td>
      </tr>
      <tr>
          <td>極限卸載</td>
          <td><code>--n-cpu-moe ~40</code></td>
          <td>接近最低</td>
          <td>上升最多</td>
          <td>較全載大幅下降</td>
      </tr>
  </tbody>
</table>
<blockquote>
<p><strong>事實查核註</strong>：上表是趨勢示意、不是經本文系統實測的數值。實際數值依顯卡型號、PCIe 版本、CUDA backend、GGUF 量化版本、<code>-ngl</code> 設定、context 長度與 batch size 變化、建議用 <code>llama-bench</code> 或實際工作流校準。</p></blockquote>
<p>社群常見的觀察是：MoE 卸載對生字速度的衰減幅度、相對於「Dense 模型把同樣比例的層卸載到 CPU」較小、原因是 MoE 每 token 只啟用少數專家、PCIe 上的權重傳輸量也較少；具體幅度依模型架構（active parameter 比例、專家數）變化。</p>
<h2 id="設計責任">設計責任</h2>
<p>理解 MoE 卸載後、可以解釋三個 PC 場景的現象：16GB VRAM 卡能載入 30B 級 MoE 模型（透過部分卸載而非全載 VRAM）、PC 場景 64GB RAM 相對 32GB 在 MoE 卸載空間上明顯更寬裕（可卸載更多層）、Mac 統一記憶體場景較少需要「卸載」這個概念（VRAM 跟 RAM 共用、不需要在兩個區域之間搬資料）。</p>
<p>設定 PC 推論伺服器時、卸載層數通常跟 KV cache 量化、context 長度、併發數一起調：先估算想開的 context 長度、扣掉 KV cache 體積算出 VRAM 餘量、再選卸載層數讓模型剛好放得進。詳見 <a href="/blog/llm/05-discrete-gpu/vram-ram-budget/" data-link-title="5.0 VRAM &#43; RAM 分層預算" data-link-desc="PC 獨立 GPU 場景的記憶體預算判讀：VRAM 是快的世界、RAM 是大的世界、PCIe 把兩個世界連起來">5.0 VRAM + RAM 分層預算</a>。</p>
]]></content:encoded></item><item><title>VRAM</title><link>https://tarrragon.github.io/blog/llm/knowledge-cards/vram/</link><pubDate>Tue, 12 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/llm/knowledge-cards/vram/</guid><description>&lt;p>VRAM（Video RAM）的核心概念是「顯卡晶片上的高速記憶體、跟系統主機板上的 RAM 是物理上獨立的兩塊預算」。獨立 GPU 場景下、模型權重要載入 VRAM 才能用 GPU 高速計算；VRAM 容量直接決定能跑多大模型。跟 Apple Silicon 的 &lt;a href="https://tarrragon.github.io/blog/llm/knowledge-cards/unified-memory/" data-link-title="Unified Memory Architecture" data-link-desc="Apple Silicon 讓 CPU / GPU / NE 共用同一塊記憶體：跑大模型的優勢來源">統一記憶體&lt;/a> 不同、PC 上 VRAM 跟系統 RAM 兩塊預算要分開規劃。&lt;/p>
&lt;h2 id="概念位置">概念位置&lt;/h2>
&lt;p>VRAM 同時影響「能載入什麼」跟「跑多快」兩個維度：&lt;/p>
&lt;ol>
&lt;li>&lt;strong>容量&lt;/strong>（GB）：決定能放多少模型權重 + &lt;a href="https://tarrragon.github.io/blog/llm/knowledge-cards/kv-cache/" data-link-title="KV Cache" data-link-desc="已處理 token 的 attention 中間結果暫存：避免重算、加速後續生成">KV cache&lt;/a> + 推論中間結果。容量不夠則跑不起來、需透過 &lt;a href="https://tarrragon.github.io/blog/llm/knowledge-cards/moe-cpu-offload/" data-link-title="MoE CPU 卸載" data-link-desc="把 Mixture-of-Experts 模型不活躍的專家層權重放在系統 RAM、用到再走 PCIe 拉回 GPU、讓有限 VRAM 跑得了更大模型">MoE CPU 卸載&lt;/a> 把部分權重放系統 RAM。&lt;/li>
&lt;li>&lt;strong>頻寬&lt;/strong>（GB/s）：影響每 token 生成速度上限、見 &lt;a href="https://tarrragon.github.io/blog/llm/knowledge-cards/memory-bandwidth/" data-link-title="Memory Bandwidth" data-link-desc="記憶體每秒能讀寫多少 bytes：決定本地 LLM 生字速度的真正瓶頸">memory bandwidth&lt;/a> 卡片。&lt;/li>
&lt;/ol>
&lt;p>常見消費級 GPU 的 VRAM 規格（廠商標稱、依世代與型號變化）：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>GPU&lt;/th>
 &lt;th>VRAM 容量&lt;/th>
 &lt;th>VRAM 類型&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>RTX 5060 / 4060&lt;/td>
 &lt;td>8GB&lt;/td>
 &lt;td>GDDR6/7&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>RTX 5060 Ti / 4060 Ti&lt;/td>
 &lt;td>16GB&lt;/td>
 &lt;td>GDDR6/7&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>RTX 5070 Ti / 4070 Ti&lt;/td>
 &lt;td>16GB&lt;/td>
 &lt;td>GDDR6/7&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>RTX 4090&lt;/td>
 &lt;td>24GB&lt;/td>
 &lt;td>GDDR6X&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>RTX 5090&lt;/td>
 &lt;td>32GB&lt;/td>
 &lt;td>GDDR7&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>VRAM 容量是選 GPU 跑本地 LLM 的第一決策軸、頻寬是第二決策軸。同容量下、頻寬接近 2 倍的卡（如 5070 Ti 對 5060 Ti）生字速度差異明顯。&lt;/p>
&lt;blockquote>
&lt;p>&lt;strong>事實查核註&lt;/strong>：上表是 2026 年 5 月主流消費級 NVIDIA GPU 規格的數量級對照、實際 VRAM 容量、頻寬、GDDR 版本依特定型號、廠商 / SKU、製造時間變化、引用前以 &lt;a href="https://www.nvidia.com/en-us/geforce/graphics-cards/">NVIDIA 官方規格頁&lt;/a> 為準。&lt;/p>&lt;/blockquote>
&lt;h2 id="設計責任">設計責任&lt;/h2>
&lt;p>理解 VRAM 後可以解釋三個現象：為什麼同樣 16GB 容量、不同卡的生字速度差很多（頻寬不同）；為什麼 MoE 模型在 16GB VRAM 上跑得了 30B 級模型（透過卸載）；為什麼 &lt;a href="https://tarrragon.github.io/blog/llm/knowledge-cards/pcie/" data-link-title="PCIe" data-link-desc="PC 上連接 GPU 跟主機板的高速序列匯流排、影響模型載入速度跟 MoE 卸載時的推論吞吐">PCIe&lt;/a> 頻寬在 PC 場景影響 MoE 卸載的速度（系統 RAM 跟 VRAM 之間的橋）。&lt;/p>
&lt;p>選 PC 規劃本地 LLM 時、VRAM 容量決定能跑的模型上限、VRAM 頻寬決定生字速度上限、系統 RAM 容量決定 MoE 卸載空間。詳見 &lt;a href="https://tarrragon.github.io/blog/llm/05-discrete-gpu/vram-ram-budget/" data-link-title="5.0 VRAM &amp;#43; RAM 分層預算" data-link-desc="PC 獨立 GPU 場景的記憶體預算判讀：VRAM 是快的世界、RAM 是大的世界、PCIe 把兩個世界連起來">5.0 VRAM + RAM 分層預算&lt;/a>。&lt;/p></description><content:encoded><![CDATA[<p>VRAM（Video RAM）的核心概念是「顯卡晶片上的高速記憶體、跟系統主機板上的 RAM 是物理上獨立的兩塊預算」。獨立 GPU 場景下、模型權重要載入 VRAM 才能用 GPU 高速計算；VRAM 容量直接決定能跑多大模型。跟 Apple Silicon 的 <a href="/blog/llm/knowledge-cards/unified-memory/" data-link-title="Unified Memory Architecture" data-link-desc="Apple Silicon 讓 CPU / GPU / NE 共用同一塊記憶體：跑大模型的優勢來源">統一記憶體</a> 不同、PC 上 VRAM 跟系統 RAM 兩塊預算要分開規劃。</p>
<h2 id="概念位置">概念位置</h2>
<p>VRAM 同時影響「能載入什麼」跟「跑多快」兩個維度：</p>
<ol>
<li><strong>容量</strong>（GB）：決定能放多少模型權重 + <a href="/blog/llm/knowledge-cards/kv-cache/" data-link-title="KV Cache" data-link-desc="已處理 token 的 attention 中間結果暫存：避免重算、加速後續生成">KV cache</a> + 推論中間結果。容量不夠則跑不起來、需透過 <a href="/blog/llm/knowledge-cards/moe-cpu-offload/" data-link-title="MoE CPU 卸載" data-link-desc="把 Mixture-of-Experts 模型不活躍的專家層權重放在系統 RAM、用到再走 PCIe 拉回 GPU、讓有限 VRAM 跑得了更大模型">MoE CPU 卸載</a> 把部分權重放系統 RAM。</li>
<li><strong>頻寬</strong>（GB/s）：影響每 token 生成速度上限、見 <a href="/blog/llm/knowledge-cards/memory-bandwidth/" data-link-title="Memory Bandwidth" data-link-desc="記憶體每秒能讀寫多少 bytes：決定本地 LLM 生字速度的真正瓶頸">memory bandwidth</a> 卡片。</li>
</ol>
<p>常見消費級 GPU 的 VRAM 規格（廠商標稱、依世代與型號變化）：</p>
<table>
  <thead>
      <tr>
          <th>GPU</th>
          <th>VRAM 容量</th>
          <th>VRAM 類型</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>RTX 5060 / 4060</td>
          <td>8GB</td>
          <td>GDDR6/7</td>
      </tr>
      <tr>
          <td>RTX 5060 Ti / 4060 Ti</td>
          <td>16GB</td>
          <td>GDDR6/7</td>
      </tr>
      <tr>
          <td>RTX 5070 Ti / 4070 Ti</td>
          <td>16GB</td>
          <td>GDDR6/7</td>
      </tr>
      <tr>
          <td>RTX 4090</td>
          <td>24GB</td>
          <td>GDDR6X</td>
      </tr>
      <tr>
          <td>RTX 5090</td>
          <td>32GB</td>
          <td>GDDR7</td>
      </tr>
  </tbody>
</table>
<p>VRAM 容量是選 GPU 跑本地 LLM 的第一決策軸、頻寬是第二決策軸。同容量下、頻寬接近 2 倍的卡（如 5070 Ti 對 5060 Ti）生字速度差異明顯。</p>
<blockquote>
<p><strong>事實查核註</strong>：上表是 2026 年 5 月主流消費級 NVIDIA GPU 規格的數量級對照、實際 VRAM 容量、頻寬、GDDR 版本依特定型號、廠商 / SKU、製造時間變化、引用前以 <a href="https://www.nvidia.com/en-us/geforce/graphics-cards/">NVIDIA 官方規格頁</a> 為準。</p></blockquote>
<h2 id="設計責任">設計責任</h2>
<p>理解 VRAM 後可以解釋三個現象：為什麼同樣 16GB 容量、不同卡的生字速度差很多（頻寬不同）；為什麼 MoE 模型在 16GB VRAM 上跑得了 30B 級模型（透過卸載）；為什麼 <a href="/blog/llm/knowledge-cards/pcie/" data-link-title="PCIe" data-link-desc="PC 上連接 GPU 跟主機板的高速序列匯流排、影響模型載入速度跟 MoE 卸載時的推論吞吐">PCIe</a> 頻寬在 PC 場景影響 MoE 卸載的速度（系統 RAM 跟 VRAM 之間的橋）。</p>
<p>選 PC 規劃本地 LLM 時、VRAM 容量決定能跑的模型上限、VRAM 頻寬決定生字速度上限、系統 RAM 容量決定 MoE 卸載空間。詳見 <a href="/blog/llm/05-discrete-gpu/vram-ram-budget/" data-link-title="5.0 VRAM &#43; RAM 分層預算" data-link-desc="PC 獨立 GPU 場景的記憶體預算判讀：VRAM 是快的世界、RAM 是大的世界、PCIe 把兩個世界連起來">5.0 VRAM + RAM 分層預算</a>。</p>
]]></content:encoded></item><item><title>3.1 GC 與 memory limit</title><link>https://tarrragon.github.io/blog/go-advanced/03-runtime-profiling/gc-memory-limit/</link><pubDate>Wed, 22 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/go-advanced/03-runtime-profiling/gc-memory-limit/</guid><description>&lt;p>GC 與 memory limit 的核心關係是：Go runtime 會根據 heap 成長決定何時執行 GC，而 memory limit 讓 runtime 有一個軟性記憶體目標。Memory limit 不是硬性上限，也不是 leak 修復工具；它是讓 runtime 更早回應記憶體壓力的控制訊號。&lt;/p>
&lt;h2 id="本章目標">本章目標&lt;/h2>
&lt;p>學完本章後，你將能夠：&lt;/p>
&lt;ol>
&lt;li>理解 heap growth、GOGC 與 GC 頻率的關係&lt;/li>
&lt;li>判斷 &lt;code>debug.SetMemoryLimit&lt;/code> 能解決什麼、不能解決什麼&lt;/li>
&lt;li>從環境變數設定服務 memory limit&lt;/li>
&lt;li>用 runtime &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/metrics/" data-link-title="Metrics" data-link-desc="說明指標如何描述服務趨勢、容量與健康狀態">metrics&lt;/a> 觀察調整效果&lt;/li>
&lt;li>分辨 GC 壓力、長期保留與真正 leak&lt;/li>
&lt;/ol>
&lt;hr>
&lt;h2 id="觀察長時間服務的記憶體問題通常是趨勢問題">【觀察】長時間服務的記憶體問題通常是趨勢問題&lt;/h2>
&lt;p>記憶體診斷的核心觀察是趨勢。Heap 是否持續上升、GC 後是否下降、goroutine 是否增加、某個操作後是否留下無法回收的資料，這些都比「現在用了多少 MB」更重要。&lt;/p>
&lt;p>常見現象：&lt;/p>
&lt;ul>
&lt;li>啟動後 heap 穩定在某個區間：通常正常。&lt;/li>
&lt;li>每次高峰後 heap 都能下降：可能是短暫配置。&lt;/li>
&lt;li>GC 後 heap 仍持續上升：可能有長期保留或 leak。&lt;/li>
&lt;li>GC 次數快速增加且 CPU 升高：可能是 allocation 壓力。&lt;/li>
&lt;li>goroutine 與 heap 同時增加：可能是 goroutine leak 或 send &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/buffer/" data-link-title="Buffer" data-link-desc="說明系統如何用暫存空間吸收短暫速度差與尖峰流量">buffer&lt;/a> 堆積。&lt;/li>
&lt;/ul>
&lt;p>Memory limit 可以幫 runtime 更積極控制 heap，但不能替代趨勢判讀。&lt;/p>
&lt;h2 id="判讀gc-控制的是-heap-成長">【判讀】GC 控制的是 heap 成長&lt;/h2>
&lt;p>Go GC 的核心目標是回收不再被引用的 heap 物件。Runtime 會根據 &lt;code>GOGC&lt;/code> 控制下一次 GC 觸發點。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="nv">GOGC&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="m">100&lt;/span> go run ./cmd/server&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>GOGC=100&lt;/code> 大致表示 heap 在上次 GC 後成長約 100% 時觸發下一次 GC。數字越小，GC 越頻繁，記憶體通常較低但 CPU 成本較高；數字越大，GC 較少，記憶體通常較高但 CPU 成本較低。&lt;/p>
&lt;p>這是取捨，不是調大或調小就一定更好。CPU 緊繃的服務可能不能承受過低 &lt;code>GOGC&lt;/code>；記憶體緊繃的服務可能不能承受過高 &lt;code>GOGC&lt;/code>。&lt;/p>
&lt;h2 id="判讀memory-limit-是-runtime-軟目標">【判讀】memory limit 是 runtime 軟目標&lt;/h2>
&lt;p>&lt;code>debug.SetMemoryLimit&lt;/code> 的核心用途是告訴 Go runtime 希望整體記憶體使用量靠近某個目標。當 runtime 接近目標時，會更積極回收 heap。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-go" data-lang="go">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="kd">func&lt;/span> &lt;span class="nf">configureRuntime&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl"> &lt;span class="kd">const&lt;/span> &lt;span class="nx">limit&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="mi">512&lt;/span> &lt;span class="o">&amp;lt;&amp;lt;&lt;/span> &lt;span class="mi">20&lt;/span> &lt;span class="c1">// 512 MiB&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl"> &lt;span class="nx">debug&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">SetMemoryLimit&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">limit&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>這不是作業系統層級的硬限制。程式仍可能短暫超過這個值，特別是有大量非 Go heap 記憶體、cgo、mmap、大型 byte slice 尖峰或外部 library 配置時。&lt;/p>
&lt;p>Memory limit 適合容器、桌面常駐服務、背景 worker、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/websocket/" data-link-title="WebSocket" data-link-desc="說明 WebSocket 如何提供長連線雙向即時通訊">WebSocket&lt;/a> server 這類需要避免吃掉過多資源的服務。若部署平台已有 memory limit，Go runtime 的 limit 通常應略低於平台限制，留給非 Go heap 與系統開銷。&lt;/p>
&lt;h2 id="執行設定值應來自部署環境">【執行】設定值應來自部署環境&lt;/h2>
&lt;p>Memory limit 的核心配置原則是由部署環境決定，而不是寫死在 library 裡。應用入口可以讀取環境變數，解析後設定 runtime。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-go" data-lang="go">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="kd">func&lt;/span> &lt;span class="nf">ConfigureMemoryLimitFromEnv&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="kt">error&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl"> &lt;span class="nx">raw&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="nx">os&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Getenv&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;APP_MEMORY_LIMIT_MB&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="nx">raw&lt;/span> &lt;span class="o">==&lt;/span> &lt;span class="s">&amp;#34;&amp;#34;&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="kc">nil&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl"> &lt;span class="nx">mb&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">err&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="nx">strconv&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Atoi&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">raw&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="nx">err&lt;/span> &lt;span class="o">!=&lt;/span> &lt;span class="kc">nil&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="nx">fmt&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Errorf&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;parse APP_MEMORY_LIMIT_MB: %w&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">err&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="nx">mb&lt;/span> &lt;span class="o">&amp;lt;=&lt;/span> &lt;span class="mi">0&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">13&lt;/span>&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="nx">fmt&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Errorf&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;APP_MEMORY_LIMIT_MB must be positive&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">14&lt;/span>&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">15&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">16&lt;/span>&lt;span class="cl"> &lt;span class="nx">debug&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">SetMemoryLimit&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nb">int64&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">mb&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">&amp;lt;&amp;lt;&lt;/span> &lt;span class="mi">20&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">17&lt;/span>&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="kc">nil&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">18&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>錯誤應在啟動時明確失敗。服務若用錯誤設定悄悄運行，後續記憶體行為會很難解釋。&lt;/p></description><content:encoded><![CDATA[<p>GC 與 memory limit 的核心關係是：Go runtime 會根據 heap 成長決定何時執行 GC，而 memory limit 讓 runtime 有一個軟性記憶體目標。Memory limit 不是硬性上限，也不是 leak 修復工具；它是讓 runtime 更早回應記憶體壓力的控制訊號。</p>
<h2 id="本章目標">本章目標</h2>
<p>學完本章後，你將能夠：</p>
<ol>
<li>理解 heap growth、GOGC 與 GC 頻率的關係</li>
<li>判斷 <code>debug.SetMemoryLimit</code> 能解決什麼、不能解決什麼</li>
<li>從環境變數設定服務 memory limit</li>
<li>用 runtime <a href="/blog/backend/knowledge-cards/metrics/" data-link-title="Metrics" data-link-desc="說明指標如何描述服務趨勢、容量與健康狀態">metrics</a> 觀察調整效果</li>
<li>分辨 GC 壓力、長期保留與真正 leak</li>
</ol>
<hr>
<h2 id="觀察長時間服務的記憶體問題通常是趨勢問題">【觀察】長時間服務的記憶體問題通常是趨勢問題</h2>
<p>記憶體診斷的核心觀察是趨勢。Heap 是否持續上升、GC 後是否下降、goroutine 是否增加、某個操作後是否留下無法回收的資料，這些都比「現在用了多少 MB」更重要。</p>
<p>常見現象：</p>
<ul>
<li>啟動後 heap 穩定在某個區間：通常正常。</li>
<li>每次高峰後 heap 都能下降：可能是短暫配置。</li>
<li>GC 後 heap 仍持續上升：可能有長期保留或 leak。</li>
<li>GC 次數快速增加且 CPU 升高：可能是 allocation 壓力。</li>
<li>goroutine 與 heap 同時增加：可能是 goroutine leak 或 send <a href="/blog/backend/knowledge-cards/buffer/" data-link-title="Buffer" data-link-desc="說明系統如何用暫存空間吸收短暫速度差與尖峰流量">buffer</a> 堆積。</li>
</ul>
<p>Memory limit 可以幫 runtime 更積極控制 heap，但不能替代趨勢判讀。</p>
<h2 id="判讀gc-控制的是-heap-成長">【判讀】GC 控制的是 heap 成長</h2>
<p>Go GC 的核心目標是回收不再被引用的 heap 物件。Runtime 會根據 <code>GOGC</code> 控制下一次 GC 觸發點。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="nv">GOGC</span><span class="o">=</span><span class="m">100</span> go run ./cmd/server</span></span></code></pre></div><p><code>GOGC=100</code> 大致表示 heap 在上次 GC 後成長約 100% 時觸發下一次 GC。數字越小，GC 越頻繁，記憶體通常較低但 CPU 成本較高；數字越大，GC 較少，記憶體通常較高但 CPU 成本較低。</p>
<p>這是取捨，不是調大或調小就一定更好。CPU 緊繃的服務可能不能承受過低 <code>GOGC</code>；記憶體緊繃的服務可能不能承受過高 <code>GOGC</code>。</p>
<h2 id="判讀memory-limit-是-runtime-軟目標">【判讀】memory limit 是 runtime 軟目標</h2>
<p><code>debug.SetMemoryLimit</code> 的核心用途是告訴 Go runtime 希望整體記憶體使用量靠近某個目標。當 runtime 接近目標時，會更積極回收 heap。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="ln">1</span><span class="cl"><span class="kd">func</span> <span class="nf">configureRuntime</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">    <span class="kd">const</span> <span class="nx">limit</span> <span class="p">=</span> <span class="mi">512</span> <span class="o">&lt;&lt;</span> <span class="mi">20</span> <span class="c1">// 512 MiB</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">    <span class="nx">debug</span><span class="p">.</span><span class="nf">SetMemoryLimit</span><span class="p">(</span><span class="nx">limit</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>這不是作業系統層級的硬限制。程式仍可能短暫超過這個值，特別是有大量非 Go heap 記憶體、cgo、mmap、大型 byte slice 尖峰或外部 library 配置時。</p>
<p>Memory limit 適合容器、桌面常駐服務、背景 worker、<a href="/blog/backend/knowledge-cards/websocket/" data-link-title="WebSocket" data-link-desc="說明 WebSocket 如何提供長連線雙向即時通訊">WebSocket</a> server 這類需要避免吃掉過多資源的服務。若部署平台已有 memory limit，Go runtime 的 limit 通常應略低於平台限制，留給非 Go heap 與系統開銷。</p>
<h2 id="執行設定值應來自部署環境">【執行】設定值應來自部署環境</h2>
<p>Memory limit 的核心配置原則是由部署環境決定，而不是寫死在 library 裡。應用入口可以讀取環境變數，解析後設定 runtime。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="kd">func</span> <span class="nf">ConfigureMemoryLimitFromEnv</span><span class="p">()</span> <span class="kt">error</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">    <span class="nx">raw</span> <span class="o">:=</span> <span class="nx">os</span><span class="p">.</span><span class="nf">Getenv</span><span class="p">(</span><span class="s">&#34;APP_MEMORY_LIMIT_MB&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">    <span class="k">if</span> <span class="nx">raw</span> <span class="o">==</span> <span class="s">&#34;&#34;</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">        <span class="k">return</span> <span class="kc">nil</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">    <span class="nx">mb</span><span class="p">,</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">strconv</span><span class="p">.</span><span class="nf">Atoi</span><span class="p">(</span><span class="nx">raw</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">    <span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">        <span class="k">return</span> <span class="nx">fmt</span><span class="p">.</span><span class="nf">Errorf</span><span class="p">(</span><span class="s">&#34;parse APP_MEMORY_LIMIT_MB: %w&#34;</span><span class="p">,</span> <span class="nx">err</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">
</span></span><span class="line"><span class="ln">12</span><span class="cl">    <span class="k">if</span> <span class="nx">mb</span> <span class="o">&lt;=</span> <span class="mi">0</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">        <span class="k">return</span> <span class="nx">fmt</span><span class="p">.</span><span class="nf">Errorf</span><span class="p">(</span><span class="s">&#34;APP_MEMORY_LIMIT_MB must be positive&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">
</span></span><span class="line"><span class="ln">16</span><span class="cl">    <span class="nx">debug</span><span class="p">.</span><span class="nf">SetMemoryLimit</span><span class="p">(</span><span class="nb">int64</span><span class="p">(</span><span class="nx">mb</span><span class="p">)</span> <span class="o">&lt;&lt;</span> <span class="mi">20</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl">    <span class="k">return</span> <span class="kc">nil</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>錯誤應在啟動時明確失敗。服務若用錯誤設定悄悄運行，後續記憶體行為會很難解釋。</p>
<h2 id="策略runtime-metrics-用來看調整是否有效">【策略】runtime metrics 用來看調整是否有效</h2>
<p>Runtime metrics 的核心用途是驗證調整效果。只改 <code>GOGC</code> 或 memory limit，不看 heap 與 GC 趨勢，容易變成憑感覺調參。</p>
<p>簡單方式可以用 <code>runtime.ReadMemStats</code>：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="ln">1</span><span class="cl"><span class="kd">func</span> <span class="nf">ReadHeapAlloc</span><span class="p">()</span> <span class="kt">uint64</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">    <span class="kd">var</span> <span class="nx">stats</span> <span class="nx">runtime</span><span class="p">.</span><span class="nx">MemStats</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">    <span class="nx">runtime</span><span class="p">.</span><span class="nf">ReadMemStats</span><span class="p">(</span><span class="o">&amp;</span><span class="nx">stats</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">    <span class="k">return</span> <span class="nx">stats</span><span class="p">.</span><span class="nx">HeapAlloc</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>較完整的服務可以使用 <code>runtime/metrics</code>：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="ln">1</span><span class="cl"><span class="kd">func</span> <span class="nf">ReadRuntimeSamples</span><span class="p">()</span> <span class="p">[]</span><span class="nx">metrics</span><span class="p">.</span><span class="nx">Sample</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">    <span class="nx">samples</span> <span class="o">:=</span> <span class="p">[]</span><span class="nx">metrics</span><span class="p">.</span><span class="nx">Sample</span><span class="p">{</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">        <span class="p">{</span><span class="nx">Name</span><span class="p">:</span> <span class="s">&#34;/memory/classes/heap/objects:bytes&#34;</span><span class="p">},</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">        <span class="p">{</span><span class="nx">Name</span><span class="p">:</span> <span class="s">&#34;/gc/cycles/total:gc-cycles&#34;</span><span class="p">},</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">        <span class="p">{</span><span class="nx">Name</span><span class="p">:</span> <span class="s">&#34;/sched/goroutines:goroutines&#34;</span><span class="p">},</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl">    <span class="nx">metrics</span><span class="p">.</span><span class="nf">Read</span><span class="p">(</span><span class="nx">samples</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl">    <span class="k">return</span> <span class="nx">samples</span>
</span></span><span class="line"><span class="ln">9</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>觀察時要看趨勢：調整後 heap 峰值是否下降、GC 次數是否合理、CPU 是否上升、goroutine 是否仍持續增加。</p>
<h2 id="判讀memory-limit-不能修正仍被引用的資料">【判讀】memory limit 不能修正仍被引用的資料</h2>
<p>Memory limit 的核心邊界是它只能影響 GC 行為，不能讓仍被引用的物件消失。若程式把資料一直留在 map、slice、cache、goroutine 或 send buffer 裡，GC 不能回收。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="ln">1</span><span class="cl"><span class="kd">var</span> <span class="nx">cache</span> <span class="p">=</span> <span class="kd">map</span><span class="p">[</span><span class="kt">string</span><span class="p">][]</span><span class="kt">byte</span><span class="p">{}</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="kd">func</span> <span class="nf">SavePayload</span><span class="p">(</span><span class="nx">id</span> <span class="kt">string</span><span class="p">,</span> <span class="nx">payload</span> <span class="p">[]</span><span class="kt">byte</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">    <span class="nx">cache</span><span class="p">[</span><span class="nx">id</span><span class="p">]</span> <span class="p">=</span> <span class="nx">payload</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>如果 <code>cache</code> 沒有大小限制、<a href="/blog/backend/knowledge-cards/ttl/" data-link-title="TTL" data-link-desc="說明資料過期時間如何影響快取新鮮度、成本與一致性">TTL</a> 或刪除策略，memory limit 只會讓 GC 更常跑，但資料仍被 <code>cache</code> 引用。真正修正是設計 cache 淘汰、分頁、快照大小限制或資料釋放路徑。</p>
<p>因此遇到 heap 持續上升時，下一步是用 pprof 確認誰保留了記憶體。</p>
<h2 id="策略判斷是-gc-壓力還是長期保留">【策略】判斷是 GC 壓力還是長期保留</h2>
<p>記憶體問題的核心分流是：物件被大量配置但很快回收，還是物件被長期保留。</p>
<table>
  <thead>
      <tr>
          <th>現象</th>
          <th>可能問題</th>
          <th>下一步</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>alloc_space</code> 高，<code>inuse_space</code> 不高</td>
          <td>短命配置多，GC 壓力大</td>
          <td>找熱路徑 allocation</td>
      </tr>
      <tr>
          <td><code>inuse_space</code> 持續上升</td>
          <td>長期保留或 leak</td>
          <td>看 heap profile retainers</td>
      </tr>
      <tr>
          <td>goroutine 數量同步上升</td>
          <td>goroutine leak 或 <a href="/blog/backend/knowledge-cards/queue/" data-link-title="Queue" data-link-desc="說明 queue 如何保存等待處理的工作並形成容量邊界">queue</a> 堆積</td>
          <td>看 goroutine profile</td>
      </tr>
      <tr>
          <td>GC 次數暴增但 heap 仍高</td>
          <td>memory limit 壓力或資料保留</td>
          <td>檢查 cache/map/buffer</td>
      </tr>
  </tbody>
</table>
<p>這個分流會決定後續工具。GC 參數能緩解壓力，但保留資料要回到資料結構與 lifecycle 修。</p>
<h2 id="測試runtime-設定函式可以獨立測解析">【測試】runtime 設定函式可以獨立測解析</h2>
<p>Runtime 本身不需要在單元測試中反覆調參。應把環境解析邏輯獨立出來，測試輸入與錯誤即可。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="kd">func</span> <span class="nf">ParseMemoryLimitMB</span><span class="p">(</span><span class="nx">raw</span> <span class="kt">string</span><span class="p">)</span> <span class="p">(</span><span class="kt">int64</span><span class="p">,</span> <span class="kt">error</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">    <span class="k">if</span> <span class="nx">raw</span> <span class="o">==</span> <span class="s">&#34;&#34;</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">        <span class="k">return</span> <span class="mi">0</span><span class="p">,</span> <span class="kc">nil</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">    <span class="nx">mb</span><span class="p">,</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">strconv</span><span class="p">.</span><span class="nf">Atoi</span><span class="p">(</span><span class="nx">raw</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">    <span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">        <span class="k">return</span> <span class="mi">0</span><span class="p">,</span> <span class="nx">fmt</span><span class="p">.</span><span class="nf">Errorf</span><span class="p">(</span><span class="s">&#34;parse memory limit: %w&#34;</span><span class="p">,</span> <span class="nx">err</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">    <span class="k">if</span> <span class="nx">mb</span> <span class="o">&lt;=</span> <span class="mi">0</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">        <span class="k">return</span> <span class="mi">0</span><span class="p">,</span> <span class="nx">fmt</span><span class="p">.</span><span class="nf">Errorf</span><span class="p">(</span><span class="s">&#34;memory limit must be positive&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">    <span class="k">return</span> <span class="nb">int64</span><span class="p">(</span><span class="nx">mb</span><span class="p">)</span> <span class="o">&lt;&lt;</span> <span class="mi">20</span><span class="p">,</span> <span class="kc">nil</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>測試：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="ln">1</span><span class="cl"><span class="kd">func</span> <span class="nf">TestParseMemoryLimitMB</span><span class="p">(</span><span class="nx">t</span> <span class="o">*</span><span class="nx">testing</span><span class="p">.</span><span class="nx">T</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">    <span class="nx">got</span><span class="p">,</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nf">ParseMemoryLimitMB</span><span class="p">(</span><span class="s">&#34;512&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">    <span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">        <span class="nx">t</span><span class="p">.</span><span class="nf">Fatalf</span><span class="p">(</span><span class="s">&#34;parse memory limit: %v&#34;</span><span class="p">,</span> <span class="nx">err</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">    <span class="k">if</span> <span class="nx">got</span> <span class="o">!=</span> <span class="mi">512</span><span class="o">&lt;&lt;</span><span class="mi">20</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl">        <span class="nx">t</span><span class="p">.</span><span class="nf">Fatalf</span><span class="p">(</span><span class="s">&#34;limit = %d, want %d&#34;</span><span class="p">,</span> <span class="nx">got</span><span class="p">,</span> <span class="nb">int64</span><span class="p">(</span><span class="mi">512</span><span class="o">&lt;&lt;</span><span class="mi">20</span><span class="p">))</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln">9</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>這讓設定邏輯可測，而不需要在每個測試中真的改 runtime 狀態。</p>
<h2 id="本章不處理">本章不處理</h2>
<p>本章先處理單一 Go process 如何判讀 heap、GC 與 memory limit；平台 OOM 與部署合約，會在下列章節再往外延伸：</p>
<ul>
<li><a href="/blog/go-advanced/07-distributed-operations/deployment-contracts/" data-link-title="7.5 Kubernetes、systemd 與 load balancer 合約" data-link-desc="理解部署平台如何影響 Go 服務的 shutdown、health 與資源限制">Go 進階：Kubernetes、systemd 與 load balancer 合約</a></li>
<li><a href="/blog/backend/05-deployment-platform/" data-link-title="模組五：部署平台與網路入口" data-link-desc="整理 Kubernetes、systemd、load balancer、container 與服務生命週期合約">Backend：部署平台與網路入口</a></li>
</ul>
<h2 id="和-go-教材的關係">和 Go 教材的關係</h2>
<p>這一章承接的是 runtime 壓力、allocation 與 pprof 診斷；如果你要先回看語言教材，可以讀：</p>
<ul>
<li><a href="/blog/go-advanced/03-runtime-profiling/allocation/" data-link-title="3.4 資料結構與 allocation 壓力" data-link-desc="分析列表、歷史資料與 WebSocket payload 的配置成本">Go：資料結構與 allocation 壓力</a></li>
<li><a href="/blog/go-advanced/03-runtime-profiling/goroutine-leak/" data-link-title="3.3 goroutine leak 偵測" data-link-desc="判斷背景工作與 client pump 是否正確退出">Go：goroutine leak 偵測</a></li>
<li><a href="/blog/go/07-refactoring/state-boundary/" data-link-title="7.4 狀態管理的安全邊界" data-link-desc="用 lock、copy 與 API 限制保護共享狀態">Go：狀態管理的安全邊界</a></li>
<li><a href="/blog/go/06-practical/new-background-worker/" data-link-title="6.4 如何新增背景工作流程" data-link-desc="接入 context、channel 與 shutdown">Go：如何新增背景工作流程</a></li>
</ul>
<h2 id="小結">小結</h2>
<p>GC 控制 heap 回收節奏，memory limit 給 runtime 一個記憶體軟目標。合理設定能降低長時間服務的資源風險，但不能修正 cache、map、slice、goroutine 或 buffer 長期持有資料。診斷時先看趨勢，再用 pprof 區分 GC 壓力與長期保留。</p>
]]></content:encoded></item><item><title>3.1 PyObject 與物件模型</title><link>https://tarrragon.github.io/blog/python-advanced/04-cpython-internals/object-model/</link><pubDate>Tue, 20 Jan 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/python-advanced/04-cpython-internals/object-model/</guid><description>&lt;p>Python 中「一切皆物件」不只是一句口號，而是 CPython 實現的核心設計。理解 PyObject 是深入 Python 內部的第一步。&lt;/p>
&lt;h2 id="先備知識">先備知識&lt;/h2>
&lt;ul>
&lt;li>進階系列 &lt;a href="https://tarrragon.github.io/blog/python-advanced/02-metaprogramming/" data-link-title="模組二：元編程" data-link-desc="深入 Python 的元編程機制，理解框架的實現原理">模組二：元編程&lt;/a>&lt;/li>
&lt;li>基本的 C 語言知識（結構體、指標）&lt;/li>
&lt;/ul>
&lt;h2 id="本章目標">本章目標&lt;/h2>
&lt;p>學完本章後，你將能夠：&lt;/p>
&lt;ol>
&lt;li>理解 PyObject 結構&lt;/li>
&lt;li>理解參考計數的工作原理&lt;/li>
&lt;li>解釋「一切皆物件」的實現方式&lt;/li>
&lt;li>觀察物件的記憶體佈局&lt;/li>
&lt;/ol>
&lt;hr>
&lt;h2 id="原理層一切皆物件">【原理層】一切皆物件&lt;/h2>
&lt;h3 id="什麼是一切皆物件">什麼是「一切皆物件」？&lt;/h3>
&lt;p>在 Python 中，所有東西都是物件：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="c1"># 數字是物件&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">&lt;span class="n">x&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="mi">42&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl">&lt;span class="nb">print&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nb">type&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">x&lt;/span>&lt;span class="p">))&lt;/span> &lt;span class="c1"># &amp;lt;class &amp;#39;int&amp;#39;&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl">&lt;span class="nb">print&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">x&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="vm">__class__&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="c1"># &amp;lt;class &amp;#39;int&amp;#39;&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl">&lt;span class="nb">print&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">x&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">bit_length&lt;/span>&lt;span class="p">())&lt;/span> &lt;span class="c1"># 6（呼叫方法）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl">&lt;span class="c1"># 函式是物件&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">&lt;span class="k">def&lt;/span> &lt;span class="nf">hello&lt;/span>&lt;span class="p">():&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl"> &lt;span class="k">pass&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">&lt;span class="nb">print&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nb">type&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">hello&lt;/span>&lt;span class="p">))&lt;/span> &lt;span class="c1"># &amp;lt;class &amp;#39;function&amp;#39;&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl">&lt;span class="nb">print&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">hello&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="vm">__name__&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="c1"># hello&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">13&lt;/span>&lt;span class="cl">&lt;span class="c1"># 類別是物件&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">14&lt;/span>&lt;span class="cl">&lt;span class="k">class&lt;/span> &lt;span class="nc">MyClass&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">15&lt;/span>&lt;span class="cl"> &lt;span class="k">pass&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">16&lt;/span>&lt;span class="cl">&lt;span class="nb">print&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nb">type&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">MyClass&lt;/span>&lt;span class="p">))&lt;/span> &lt;span class="c1"># &amp;lt;class &amp;#39;type&amp;#39;&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">17&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">18&lt;/span>&lt;span class="cl">&lt;span class="c1"># 甚至 type 本身也是物件&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">19&lt;/span>&lt;span class="cl">&lt;span class="nb">print&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nb">type&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nb">type&lt;/span>&lt;span class="p">))&lt;/span> &lt;span class="c1"># &amp;lt;class &amp;#39;type&amp;#39;&amp;gt;&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="pyobject-結構">PyObject 結構&lt;/h3>
&lt;p>在 C 語言層面，所有 Python 物件都基於 &lt;code>PyObject&lt;/code> 結構：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-c" data-lang="c">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="c1">// CPython 原始碼中的定義（簡化版）
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="k">typedef&lt;/span> &lt;span class="k">struct&lt;/span> &lt;span class="n">_object&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl"> &lt;span class="n">Py_ssize_t&lt;/span> &lt;span class="n">ob_refcnt&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="c1">// 參考計數
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="n">PyTypeObject&lt;/span> &lt;span class="o">*&lt;/span>&lt;span class="n">ob_type&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="c1">// 型別指標
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="p">}&lt;/span> &lt;span class="n">PyObject&lt;/span>&lt;span class="p">;&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>每個 Python 物件在記憶體中至少包含這兩個欄位：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">┌─────────────────────────────────────┐
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">│ PyObject │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">├─────────────────────────────────────┤
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">│ ob_refcnt (參考計數) 8 bytes │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">│ ob_type (型別指標) 8 bytes │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">├─────────────────────────────────────┤
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl">│ ... 物件特定的資料 ... │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">8&lt;/span>&lt;span class="cl">└─────────────────────────────────────┘&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="變長物件pyvarobject">變長物件：PyVarObject&lt;/h3>
&lt;p>對於長度可變的物件（如 list、str），使用 &lt;code>PyVarObject&lt;/code>：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-c" data-lang="c">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="k">typedef&lt;/span> &lt;span class="k">struct&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl"> &lt;span class="n">PyObject&lt;/span> &lt;span class="n">ob_base&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl"> &lt;span class="n">Py_ssize_t&lt;/span> &lt;span class="n">ob_size&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="c1">// 元素數量
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="p">}&lt;/span> &lt;span class="n">PyVarObject&lt;/span>&lt;span class="p">;&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>




&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">┌─────────────────────────────────────┐
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">│ PyVarObject │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">├─────────────────────────────────────┤
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">│ ob_refcnt (參考計數) 8 bytes │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">│ ob_type (型別指標) 8 bytes │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">│ ob_size (元素數量) 8 bytes │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl">├─────────────────────────────────────┤
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">8&lt;/span>&lt;span class="cl">│ ... 元素資料 ... │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">9&lt;/span>&lt;span class="cl">└─────────────────────────────────────┘&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;hr>
&lt;h2 id="設計層參考計數">【設計層】參考計數&lt;/h2>
&lt;h3 id="工作原理">工作原理&lt;/h3>
&lt;p>Python 使用參考計數來追蹤物件的使用：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="kn">import&lt;/span> &lt;span class="nn">sys&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl">&lt;span class="n">a&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="mi">1&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">2&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">3&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl">&lt;span class="nb">print&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">sys&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">getrefcount&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">a&lt;/span>&lt;span class="p">))&lt;/span> &lt;span class="c1"># 2（a 本身 + getrefcount 的參數）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">&lt;span class="n">b&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">a&lt;/span> &lt;span class="c1"># 增加參考&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl">&lt;span class="nb">print&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">sys&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">getrefcount&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">a&lt;/span>&lt;span class="p">))&lt;/span> &lt;span class="c1"># 3&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">&lt;span class="k">del&lt;/span> &lt;span class="n">b&lt;/span> &lt;span class="c1"># 減少參考&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">&lt;span class="nb">print&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">sys&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">getrefcount&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">a&lt;/span>&lt;span class="p">))&lt;/span> &lt;span class="c1"># 2&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="參考計數的增減時機">參考計數的增減時機&lt;/h3>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="c1"># 增加參考計數的操作&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">&lt;span class="n">x&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">obj&lt;/span> &lt;span class="c1"># 賦值&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl">&lt;span class="n">container&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">append&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">obj&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="c1"># 加入容器&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl">&lt;span class="n">func&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">obj&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="c1"># 作為參數傳遞&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">&lt;span class="c1"># 減少參考計數的操作&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl">&lt;span class="k">del&lt;/span> &lt;span class="n">x&lt;/span> &lt;span class="c1"># 刪除變數&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">&lt;span class="n">x&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">other&lt;/span> &lt;span class="c1"># 重新賦值&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">&lt;span class="n">container&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">remove&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">obj&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="c1"># 從容器移除&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">&lt;span class="n">函式返回&lt;/span> &lt;span class="c1"># 區域變數離開作用域&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="參考計數的優缺點">參考計數的優缺點&lt;/h3>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>優點&lt;/th>
 &lt;th>缺點&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>即時回收&lt;/td>
 &lt;td>無法處理循環參考&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>可預測的記憶體使用&lt;/td>
 &lt;td>每次操作都要更新計數&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>簡單易理解&lt;/td>
 &lt;td>多執行緒下需要鎖（GIL 的原因之一）&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;hr>
&lt;h2 id="實作層觀察物件">【實作層】觀察物件&lt;/h2>
&lt;h3 id="使用-id-觀察記憶體位址">使用 id() 觀察記憶體位址&lt;/h3>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="n">a&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="mi">1&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">2&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">3&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">&lt;span class="n">b&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">a&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl">&lt;span class="n">c&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="mi">1&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">2&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">3&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl">&lt;span class="nb">print&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nb">id&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">a&lt;/span>&lt;span class="p">))&lt;/span> &lt;span class="c1"># 140234567890112&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">&lt;span class="nb">print&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nb">id&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">b&lt;/span>&lt;span class="p">))&lt;/span> &lt;span class="c1"># 140234567890112（同一物件）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl">&lt;span class="nb">print&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nb">id&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">c&lt;/span>&lt;span class="p">))&lt;/span> &lt;span class="c1"># 140234567890176（不同物件）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">&lt;span class="nb">print&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">a&lt;/span> &lt;span class="ow">is&lt;/span> &lt;span class="n">b&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="c1"># True&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">&lt;span class="nb">print&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">a&lt;/span> &lt;span class="ow">is&lt;/span> &lt;span class="n">c&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="c1"># False&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl">&lt;span class="nb">print&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">a&lt;/span> &lt;span class="o">==&lt;/span> &lt;span class="n">c&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="c1"># True（值相等）&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="小整數快取">小整數快取&lt;/h3>
&lt;p>CPython 對 -5 到 256 的整數進行快取：&lt;/p></description><content:encoded><![CDATA[<p>Python 中「一切皆物件」不只是一句口號，而是 CPython 實現的核心設計。理解 PyObject 是深入 Python 內部的第一步。</p>
<h2 id="先備知識">先備知識</h2>
<ul>
<li>進階系列 <a href="/blog/python-advanced/02-metaprogramming/" data-link-title="模組二：元編程" data-link-desc="深入 Python 的元編程機制，理解框架的實現原理">模組二：元編程</a></li>
<li>基本的 C 語言知識（結構體、指標）</li>
</ul>
<h2 id="本章目標">本章目標</h2>
<p>學完本章後，你將能夠：</p>
<ol>
<li>理解 PyObject 結構</li>
<li>理解參考計數的工作原理</li>
<li>解釋「一切皆物件」的實現方式</li>
<li>觀察物件的記憶體佈局</li>
</ol>
<hr>
<h2 id="原理層一切皆物件">【原理層】一切皆物件</h2>
<h3 id="什麼是一切皆物件">什麼是「一切皆物件」？</h3>
<p>在 Python 中，所有東西都是物件：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1"># 數字是物件</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="n">x</span> <span class="o">=</span> <span class="mi">42</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="nb">print</span><span class="p">(</span><span class="nb">type</span><span class="p">(</span><span class="n">x</span><span class="p">))</span>        <span class="c1"># &lt;class &#39;int&#39;&gt;</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="nb">print</span><span class="p">(</span><span class="n">x</span><span class="o">.</span><span class="vm">__class__</span><span class="p">)</span>    <span class="c1"># &lt;class &#39;int&#39;&gt;</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="nb">print</span><span class="p">(</span><span class="n">x</span><span class="o">.</span><span class="n">bit_length</span><span class="p">())</span> <span class="c1"># 6（呼叫方法）</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="c1"># 函式是物件</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="k">def</span> <span class="nf">hello</span><span class="p">():</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">    <span class="k">pass</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="nb">print</span><span class="p">(</span><span class="nb">type</span><span class="p">(</span><span class="n">hello</span><span class="p">))</span>    <span class="c1"># &lt;class &#39;function&#39;&gt;</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="nb">print</span><span class="p">(</span><span class="n">hello</span><span class="o">.</span><span class="vm">__name__</span><span class="p">)</span> <span class="c1"># hello</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">
</span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="c1"># 類別是物件</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="k">class</span> <span class="nc">MyClass</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">    <span class="k">pass</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="nb">print</span><span class="p">(</span><span class="nb">type</span><span class="p">(</span><span class="n">MyClass</span><span class="p">))</span>  <span class="c1"># &lt;class &#39;type&#39;&gt;</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl">
</span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="c1"># 甚至 type 本身也是物件</span>
</span></span><span class="line"><span class="ln">19</span><span class="cl"><span class="nb">print</span><span class="p">(</span><span class="nb">type</span><span class="p">(</span><span class="nb">type</span><span class="p">))</span>     <span class="c1"># &lt;class &#39;type&#39;&gt;</span></span></span></code></pre></div><h3 id="pyobject-結構">PyObject 結構</h3>
<p>在 C 語言層面，所有 Python 物件都基於 <code>PyObject</code> 結構：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-c" data-lang="c"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">// CPython 原始碼中的定義（簡化版）
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="k">typedef</span> <span class="k">struct</span> <span class="n">_object</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">    <span class="n">Py_ssize_t</span> <span class="n">ob_refcnt</span><span class="p">;</span>  <span class="c1">// 參考計數
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1"></span>    <span class="n">PyTypeObject</span> <span class="o">*</span><span class="n">ob_type</span><span class="p">;</span> <span class="c1">// 型別指標
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1"></span><span class="p">}</span> <span class="n">PyObject</span><span class="p">;</span></span></span></code></pre></div><p>每個 Python 物件在記憶體中至少包含這兩個欄位：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">┌─────────────────────────────────────┐
</span></span><span class="line"><span class="ln">2</span><span class="cl">│           PyObject                   │
</span></span><span class="line"><span class="ln">3</span><span class="cl">├─────────────────────────────────────┤
</span></span><span class="line"><span class="ln">4</span><span class="cl">│  ob_refcnt (參考計數)    8 bytes    │
</span></span><span class="line"><span class="ln">5</span><span class="cl">│  ob_type   (型別指標)    8 bytes    │
</span></span><span class="line"><span class="ln">6</span><span class="cl">├─────────────────────────────────────┤
</span></span><span class="line"><span class="ln">7</span><span class="cl">│  ... 物件特定的資料 ...              │
</span></span><span class="line"><span class="ln">8</span><span class="cl">└─────────────────────────────────────┘</span></span></code></pre></div><h3 id="變長物件pyvarobject">變長物件：PyVarObject</h3>
<p>對於長度可變的物件（如 list、str），使用 <code>PyVarObject</code>：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-c" data-lang="c"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">typedef</span> <span class="k">struct</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">    <span class="n">PyObject</span> <span class="n">ob_base</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">    <span class="n">Py_ssize_t</span> <span class="n">ob_size</span><span class="p">;</span>  <span class="c1">// 元素數量
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1"></span><span class="p">}</span> <span class="n">PyVarObject</span><span class="p">;</span></span></span></code></pre></div>




<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">┌─────────────────────────────────────┐
</span></span><span class="line"><span class="ln">2</span><span class="cl">│         PyVarObject                  │
</span></span><span class="line"><span class="ln">3</span><span class="cl">├─────────────────────────────────────┤
</span></span><span class="line"><span class="ln">4</span><span class="cl">│  ob_refcnt (參考計數)    8 bytes    │
</span></span><span class="line"><span class="ln">5</span><span class="cl">│  ob_type   (型別指標)    8 bytes    │
</span></span><span class="line"><span class="ln">6</span><span class="cl">│  ob_size   (元素數量)    8 bytes    │
</span></span><span class="line"><span class="ln">7</span><span class="cl">├─────────────────────────────────────┤
</span></span><span class="line"><span class="ln">8</span><span class="cl">│  ... 元素資料 ...                    │
</span></span><span class="line"><span class="ln">9</span><span class="cl">└─────────────────────────────────────┘</span></span></code></pre></div><hr>
<h2 id="設計層參考計數">【設計層】參考計數</h2>
<h3 id="工作原理">工作原理</h3>
<p>Python 使用參考計數來追蹤物件的使用：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="kn">import</span> <span class="nn">sys</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="n">a</span> <span class="o">=</span> <span class="p">[</span><span class="mi">1</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span> <span class="mi">3</span><span class="p">]</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="nb">print</span><span class="p">(</span><span class="n">sys</span><span class="o">.</span><span class="n">getrefcount</span><span class="p">(</span><span class="n">a</span><span class="p">))</span>  <span class="c1"># 2（a 本身 + getrefcount 的參數）</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="n">b</span> <span class="o">=</span> <span class="n">a</span>  <span class="c1"># 增加參考</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="nb">print</span><span class="p">(</span><span class="n">sys</span><span class="o">.</span><span class="n">getrefcount</span><span class="p">(</span><span class="n">a</span><span class="p">))</span>  <span class="c1"># 3</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="k">del</span> <span class="n">b</span>  <span class="c1"># 減少參考</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="nb">print</span><span class="p">(</span><span class="n">sys</span><span class="o">.</span><span class="n">getrefcount</span><span class="p">(</span><span class="n">a</span><span class="p">))</span>  <span class="c1"># 2</span></span></span></code></pre></div><h3 id="參考計數的增減時機">參考計數的增減時機</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1"># 增加參考計數的操作</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="n">x</span> <span class="o">=</span> <span class="n">obj</span>          <span class="c1"># 賦值</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="n">container</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="n">obj</span><span class="p">)</span>  <span class="c1"># 加入容器</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="n">func</span><span class="p">(</span><span class="n">obj</span><span class="p">)</span>        <span class="c1"># 作為參數傳遞</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="c1"># 減少參考計數的操作</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="k">del</span> <span class="n">x</span>            <span class="c1"># 刪除變數</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="n">x</span> <span class="o">=</span> <span class="n">other</span>        <span class="c1"># 重新賦值</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="n">container</span><span class="o">.</span><span class="n">remove</span><span class="p">(</span><span class="n">obj</span><span class="p">)</span>  <span class="c1"># 從容器移除</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="n">函式返回</span>         <span class="c1"># 區域變數離開作用域</span></span></span></code></pre></div><h3 id="參考計數的優缺點">參考計數的優缺點</h3>
<table>
  <thead>
      <tr>
          <th>優點</th>
          <th>缺點</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>即時回收</td>
          <td>無法處理循環參考</td>
      </tr>
      <tr>
          <td>可預測的記憶體使用</td>
          <td>每次操作都要更新計數</td>
      </tr>
      <tr>
          <td>簡單易理解</td>
          <td>多執行緒下需要鎖（GIL 的原因之一）</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="實作層觀察物件">【實作層】觀察物件</h2>
<h3 id="使用-id-觀察記憶體位址">使用 id() 觀察記憶體位址</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="n">a</span> <span class="o">=</span> <span class="p">[</span><span class="mi">1</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span> <span class="mi">3</span><span class="p">]</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="n">b</span> <span class="o">=</span> <span class="n">a</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="n">c</span> <span class="o">=</span> <span class="p">[</span><span class="mi">1</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span> <span class="mi">3</span><span class="p">]</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">
</span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="nb">print</span><span class="p">(</span><span class="nb">id</span><span class="p">(</span><span class="n">a</span><span class="p">))</span>  <span class="c1"># 140234567890112</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="nb">print</span><span class="p">(</span><span class="nb">id</span><span class="p">(</span><span class="n">b</span><span class="p">))</span>  <span class="c1"># 140234567890112（同一物件）</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="nb">print</span><span class="p">(</span><span class="nb">id</span><span class="p">(</span><span class="n">c</span><span class="p">))</span>  <span class="c1"># 140234567890176（不同物件）</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="nb">print</span><span class="p">(</span><span class="n">a</span> <span class="ow">is</span> <span class="n">b</span><span class="p">)</span>  <span class="c1"># True</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="nb">print</span><span class="p">(</span><span class="n">a</span> <span class="ow">is</span> <span class="n">c</span><span class="p">)</span>  <span class="c1"># False</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="nb">print</span><span class="p">(</span><span class="n">a</span> <span class="o">==</span> <span class="n">c</span><span class="p">)</span>  <span class="c1"># True（值相等）</span></span></span></code></pre></div><h3 id="小整數快取">小整數快取</h3>
<p>CPython 對 -5 到 256 的整數進行快取：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="n">a</span> <span class="o">=</span> <span class="mi">256</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="n">b</span> <span class="o">=</span> <span class="mi">256</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="nb">print</span><span class="p">(</span><span class="n">a</span> <span class="ow">is</span> <span class="n">b</span><span class="p">)</span>  <span class="c1"># True（同一物件）</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">
</span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="n">a</span> <span class="o">=</span> <span class="mi">257</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="n">b</span> <span class="o">=</span> <span class="mi">257</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="nb">print</span><span class="p">(</span><span class="n">a</span> <span class="ow">is</span> <span class="n">b</span><span class="p">)</span>  <span class="c1"># False（不同物件）</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="c1"># 但在同一行的情況可能會被編譯器優化</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="n">a</span><span class="p">,</span> <span class="n">b</span> <span class="o">=</span> <span class="mi">257</span><span class="p">,</span> <span class="mi">257</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="nb">print</span><span class="p">(</span><span class="n">a</span> <span class="ow">is</span> <span class="n">b</span><span class="p">)</span>  <span class="c1"># True（編譯時優化）</span></span></span></code></pre></div><h3 id="字串駐留string-interning">字串駐留（String Interning）</h3>
<p>簡單的字串會被自動駐留：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="n">a</span> <span class="o">=</span> <span class="s2">&#34;hello&#34;</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="n">b</span> <span class="o">=</span> <span class="s2">&#34;hello&#34;</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="nb">print</span><span class="p">(</span><span class="n">a</span> <span class="ow">is</span> <span class="n">b</span><span class="p">)</span>  <span class="c1"># True（駐留）</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">
</span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="n">a</span> <span class="o">=</span> <span class="s2">&#34;hello world&#34;</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="n">b</span> <span class="o">=</span> <span class="s2">&#34;hello world&#34;</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="nb">print</span><span class="p">(</span><span class="n">a</span> <span class="ow">is</span> <span class="n">b</span><span class="p">)</span>  <span class="c1"># False（含空格，不駐留）</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="c1"># 手動駐留</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="kn">import</span> <span class="nn">sys</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="n">a</span> <span class="o">=</span> <span class="n">sys</span><span class="o">.</span><span class="n">intern</span><span class="p">(</span><span class="s2">&#34;hello world&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="n">b</span> <span class="o">=</span> <span class="n">sys</span><span class="o">.</span><span class="n">intern</span><span class="p">(</span><span class="s2">&#34;hello world&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="nb">print</span><span class="p">(</span><span class="n">a</span> <span class="ow">is</span> <span class="n">b</span><span class="p">)</span>  <span class="c1"># True</span></span></span></code></pre></div><h3 id="使用-ctypes-觀察記憶體">使用 ctypes 觀察記憶體</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="kn">import</span> <span class="nn">ctypes</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="kn">import</span> <span class="nn">sys</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="k">def</span> <span class="nf">get_refcount</span><span class="p">(</span><span class="n">obj_id</span><span class="p">):</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">    <span class="s2">&#34;&#34;&#34;直接從記憶體讀取參考計數&#34;&#34;&#34;</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">    <span class="k">return</span> <span class="n">ctypes</span><span class="o">.</span><span class="n">c_long</span><span class="o">.</span><span class="n">from_address</span><span class="p">(</span><span class="n">obj_id</span><span class="p">)</span><span class="o">.</span><span class="n">value</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="n">a</span> <span class="o">=</span> <span class="p">[</span><span class="mi">1</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span> <span class="mi">3</span><span class="p">]</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="n">obj_id</span> <span class="o">=</span> <span class="nb">id</span><span class="p">(</span><span class="n">a</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="nb">print</span><span class="p">(</span><span class="sa">f</span><span class="s2">&#34;sys.getrefcount: </span><span class="si">{</span><span class="n">sys</span><span class="o">.</span><span class="n">getrefcount</span><span class="p">(</span><span class="n">a</span><span class="p">)</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="nb">print</span><span class="p">(</span><span class="sa">f</span><span class="s2">&#34;ctypes 直接讀取: </span><span class="si">{</span><span class="n">get_refcount</span><span class="p">(</span><span class="n">obj_id</span><span class="p">)</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="c1"># 注意：sys.getrefcount 會多 1（因為參數傳遞）</span></span></span></code></pre></div><h3 id="觀察物件大小">觀察物件大小</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="kn">import</span> <span class="nn">sys</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="c1"># 基本物件大小</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="nb">print</span><span class="p">(</span><span class="n">sys</span><span class="o">.</span><span class="n">getsizeof</span><span class="p">(</span><span class="kc">None</span><span class="p">))</span>      <span class="c1"># 16</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="nb">print</span><span class="p">(</span><span class="n">sys</span><span class="o">.</span><span class="n">getsizeof</span><span class="p">(</span><span class="kc">True</span><span class="p">))</span>      <span class="c1"># 28</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="nb">print</span><span class="p">(</span><span class="n">sys</span><span class="o">.</span><span class="n">getsizeof</span><span class="p">(</span><span class="mi">0</span><span class="p">))</span>         <span class="c1"># 28</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="nb">print</span><span class="p">(</span><span class="n">sys</span><span class="o">.</span><span class="n">getsizeof</span><span class="p">(</span><span class="mi">1</span><span class="p">))</span>         <span class="c1"># 28</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="nb">print</span><span class="p">(</span><span class="n">sys</span><span class="o">.</span><span class="n">getsizeof</span><span class="p">(</span><span class="mi">10</span><span class="o">**</span><span class="mi">100</span><span class="p">))</span>   <span class="c1"># 72（大整數）</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="c1"># 容器大小（不包含元素）</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="nb">print</span><span class="p">(</span><span class="n">sys</span><span class="o">.</span><span class="n">getsizeof</span><span class="p">([]))</span>        <span class="c1"># 56</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="nb">print</span><span class="p">(</span><span class="n">sys</span><span class="o">.</span><span class="n">getsizeof</span><span class="p">([</span><span class="mi">1</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span> <span class="mi">3</span><span class="p">]))</span> <span class="c1"># 88</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="nb">print</span><span class="p">(</span><span class="n">sys</span><span class="o">.</span><span class="n">getsizeof</span><span class="p">({}))</span>        <span class="c1"># 64</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">
</span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="c1"># 注意：getsizeof 不遞迴計算</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="n">nested</span> <span class="o">=</span> <span class="p">[[</span><span class="mi">1</span><span class="p">,</span> <span class="mi">2</span><span class="p">],</span> <span class="p">[</span><span class="mi">3</span><span class="p">,</span> <span class="mi">4</span><span class="p">]]</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="nb">print</span><span class="p">(</span><span class="n">sys</span><span class="o">.</span><span class="n">getsizeof</span><span class="p">(</span><span class="n">nested</span><span class="p">))</span>    <span class="c1"># 只計算外層 list</span></span></span></code></pre></div><hr>
<h2 id="深入pytypeobject">【深入】PyTypeObject</h2>
<h3 id="型別物件的結構">型別物件的結構</h3>
<p>每個型別（int、str、list 等）都是 <code>PyTypeObject</code> 的實例：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-c" data-lang="c"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1">// 簡化版
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1"></span><span class="k">typedef</span> <span class="k">struct</span> <span class="n">_typeobject</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">    <span class="n">PyObject_VAR_HEAD</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">    <span class="k">const</span> <span class="kt">char</span> <span class="o">*</span><span class="n">tp_name</span><span class="p">;</span>       <span class="c1">// 型別名稱
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="c1"></span>    <span class="n">Py_ssize_t</span> <span class="n">tp_basicsize</span><span class="p">;</span>   <span class="c1">// 基本大小
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="c1"></span>    <span class="n">Py_ssize_t</span> <span class="n">tp_itemsize</span><span class="p">;</span>    <span class="c1">// 元素大小（變長物件）
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="c1"></span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">    <span class="c1">// 方法槽（slots）
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="c1"></span>    <span class="n">destructor</span> <span class="n">tp_dealloc</span><span class="p">;</span>     <span class="c1">// 解構函式
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="c1"></span>    <span class="n">reprfunc</span> <span class="n">tp_repr</span><span class="p">;</span>          <span class="c1">// __repr__
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="c1"></span>    <span class="n">hashfunc</span> <span class="n">tp_hash</span><span class="p">;</span>          <span class="c1">// __hash__
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="c1"></span>    <span class="c1">// ... 更多方法槽
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="c1"></span><span class="p">}</span> <span class="n">PyTypeObject</span><span class="p">;</span></span></span></code></pre></div><h3 id="在-python-中觀察型別資訊">在 Python 中觀察型別資訊</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1"># 型別的基本資訊</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="nb">print</span><span class="p">(</span><span class="nb">int</span><span class="o">.</span><span class="vm">__name__</span><span class="p">)</span>       <span class="c1"># int</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="nb">print</span><span class="p">(</span><span class="nb">int</span><span class="o">.</span><span class="n">__basicsize__</span><span class="p">)</span>  <span class="c1"># 28（64-bit 系統）</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">
</span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="c1"># 方法解析順序</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="k">class</span> <span class="nc">A</span><span class="p">:</span> <span class="k">pass</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="k">class</span> <span class="nc">B</span><span class="p">(</span><span class="n">A</span><span class="p">):</span> <span class="k">pass</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="k">class</span> <span class="nc">C</span><span class="p">(</span><span class="n">B</span><span class="p">):</span> <span class="k">pass</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="nb">print</span><span class="p">(</span><span class="n">C</span><span class="o">.</span><span class="vm">__mro__</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="c1"># (&lt;class &#39;C&#39;&gt;, &lt;class &#39;B&#39;&gt;, &lt;class &#39;A&#39;&gt;, &lt;class &#39;object&#39;&gt;)</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">
</span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="c1"># 型別的型別</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="nb">print</span><span class="p">(</span><span class="nb">type</span><span class="p">(</span><span class="nb">int</span><span class="p">))</span>    <span class="c1"># &lt;class &#39;type&#39;&gt;</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="nb">print</span><span class="p">(</span><span class="nb">type</span><span class="p">(</span><span class="nb">type</span><span class="p">))</span>   <span class="c1"># &lt;class &#39;type&#39;&gt;（type 是自己的實例）</span></span></span></code></pre></div><h3 id="為什麼-is-比--快">為什麼 is 比 == 快？</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1"># is 只比較記憶體位址（一個指標比較）</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1"># == 需要呼叫 __eq__ 方法（可能很複雜）</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="kn">import</span> <span class="nn">timeit</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="n">a</span> <span class="o">=</span> <span class="p">[</span><span class="mi">1</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span> <span class="mi">3</span><span class="p">]</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="n">b</span> <span class="o">=</span> <span class="n">a</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="n">c</span> <span class="o">=</span> <span class="p">[</span><span class="mi">1</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span> <span class="mi">3</span><span class="p">]</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="c1"># is 比較（非常快）</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="nb">print</span><span class="p">(</span><span class="n">timeit</span><span class="o">.</span><span class="n">timeit</span><span class="p">(</span><span class="s1">&#39;a is b&#39;</span><span class="p">,</span> <span class="nb">globals</span><span class="o">=</span><span class="nb">globals</span><span class="p">(),</span> <span class="n">number</span><span class="o">=</span><span class="mi">1000000</span><span class="p">))</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="c1"># 約 0.02 秒</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">
</span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="c1"># == 比較（需要比較內容）</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="nb">print</span><span class="p">(</span><span class="n">timeit</span><span class="o">.</span><span class="n">timeit</span><span class="p">(</span><span class="s1">&#39;a == c&#39;</span><span class="p">,</span> <span class="nb">globals</span><span class="o">=</span><span class="nb">globals</span><span class="p">(),</span> <span class="n">number</span><span class="o">=</span><span class="mi">1000000</span><span class="p">))</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="c1"># 約 0.05 秒</span></span></span></code></pre></div><hr>
<h2 id="實戰效能影響">【實戰】效能影響</h2>
<h3 id="避免不必要的物件建立">避免不必要的物件建立</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1"># 不好：每次迭代都建立新的 tuple</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="mi">1000</span><span class="p">):</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">    <span class="n">point</span> <span class="o">=</span> <span class="p">(</span><span class="n">i</span><span class="p">,</span> <span class="n">i</span> <span class="o">*</span> <span class="mi">2</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">
</span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="c1"># 好：如果結構固定，考慮使用 __slots__ 的類別</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="k">class</span> <span class="nc">Point</span><span class="p">:</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">    <span class="vm">__slots__</span> <span class="o">=</span> <span class="p">[</span><span class="s1">&#39;x&#39;</span><span class="p">,</span> <span class="s1">&#39;y&#39;</span><span class="p">]</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">    <span class="k">def</span> <span class="fm">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">x</span><span class="p">,</span> <span class="n">y</span><span class="p">):</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">x</span> <span class="o">=</span> <span class="n">x</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">y</span> <span class="o">=</span> <span class="n">y</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">
</span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="c1"># 或者使用 namedtuple</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="kn">from</span> <span class="nn">collections</span> <span class="kn">import</span> <span class="n">namedtuple</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="n">Point</span> <span class="o">=</span> <span class="n">namedtuple</span><span class="p">(</span><span class="s1">&#39;Point&#39;</span><span class="p">,</span> <span class="p">[</span><span class="s1">&#39;x&#39;</span><span class="p">,</span> <span class="s1">&#39;y&#39;</span><span class="p">])</span></span></span></code></pre></div><h3 id="使用物件池">使用物件池</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1"># 對於頻繁建立的小物件，考慮重複使用</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="k">class</span> <span class="nc">ObjectPool</span><span class="p">:</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">    <span class="k">def</span> <span class="fm">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">factory</span><span class="p">,</span> <span class="n">max_size</span><span class="o">=</span><span class="mi">100</span><span class="p">):</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">_factory</span> <span class="o">=</span> <span class="n">factory</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">_pool</span> <span class="o">=</span> <span class="p">[]</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">_max_size</span> <span class="o">=</span> <span class="n">max_size</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">    <span class="k">def</span> <span class="nf">acquire</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">        <span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">_pool</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">            <span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_pool</span><span class="o">.</span><span class="n">pop</span><span class="p">()</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">        <span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_factory</span><span class="p">()</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">
</span></span><span class="line"><span class="ln">13</span><span class="cl">    <span class="k">def</span> <span class="nf">release</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">obj</span><span class="p">):</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">        <span class="k">if</span> <span class="nb">len</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">_pool</span><span class="p">)</span> <span class="o">&lt;</span> <span class="bp">self</span><span class="o">.</span><span class="n">_max_size</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">            <span class="bp">self</span><span class="o">.</span><span class="n">_pool</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="n">obj</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">
</span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="c1"># 使用</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="n">pool</span> <span class="o">=</span> <span class="n">ObjectPool</span><span class="p">(</span><span class="nb">list</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">19</span><span class="cl"><span class="n">lst</span> <span class="o">=</span> <span class="n">pool</span><span class="o">.</span><span class="n">acquire</span><span class="p">()</span>
</span></span><span class="line"><span class="ln">20</span><span class="cl"><span class="n">lst</span><span class="o">.</span><span class="n">extend</span><span class="p">([</span><span class="mi">1</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span> <span class="mi">3</span><span class="p">])</span>
</span></span><span class="line"><span class="ln">21</span><span class="cl"><span class="c1"># 使用完畢</span>
</span></span><span class="line"><span class="ln">22</span><span class="cl"><span class="n">lst</span><span class="o">.</span><span class="n">clear</span><span class="p">()</span>
</span></span><span class="line"><span class="ln">23</span><span class="cl"><span class="n">pool</span><span class="o">.</span><span class="n">release</span><span class="p">(</span><span class="n">lst</span><span class="p">)</span></span></span></code></pre></div><hr>
<h2 id="思考題">思考題</h2>
<ol>
<li>為什麼 CPython 選擇 -5 到 256 作為小整數快取的範圍？</li>
<li>如果參考計數是 Python 物件的核心，那多執行緒時會發生什麼問題？</li>
<li><code>None</code> 是單例，這是如何實現的？</li>
</ol>
<h2 id="實作練習">實作練習</h2>
<ol>
<li>寫一個函式，計算一個巢狀資料結構的「真實」記憶體使用量</li>
<li>使用 <code>ctypes</code> 觀察 list 物件的內部結構</li>
<li>實驗不同大小的整數的 <code>is</code> 行為</li>
</ol>
<h2 id="延伸閱讀">延伸閱讀</h2>
<ul>
<li><a href="https://github.com/python/cpython/blob/main/Include/object.h">CPython Source - object.h</a></li>
<li><a href="https://realpython.com/python-memory-management/">Real Python - Python Memory Management</a></li>
</ul>
<hr>
<p>下一章：<a href="/blog/python-advanced/04-cpython-internals/memory-gc/" data-link-title="3.2 記憶體管理與垃圾回收" data-link-desc="理解 Python 的記憶體管理機制">記憶體管理與垃圾回收</a></p>
]]></content:encoded></item><item><title>案例：記憶體優化</title><link>https://tarrragon.github.io/blog/python-advanced/04-cpython-internals/case-studies/memory-optimization/</link><pubDate>Wed, 21 Jan 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/python-advanced/04-cpython-internals/case-studies/memory-optimization/</guid><description>&lt;p>本案例基於 &lt;code>.claude/lib/config_loader.py&lt;/code> 的實際程式碼，展示如何用 &lt;code>__slots__&lt;/code> 和 &lt;code>weakref&lt;/code> 優化記憶體使用。&lt;/p>
&lt;h2 id="先備知識">先備知識&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/python-advanced/04-cpython-internals/" data-link-title="模組四：CPython 內部機制" data-link-desc="深入 CPython 直譯器，理解 Python 如何運作">模組四：CPython 內部機制&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/python-advanced/04-cpython-internals/memory-gc/" data-link-title="3.2 記憶體管理與垃圾回收" data-link-desc="理解 Python 的記憶體管理機制">4.2 記憶體管理與垃圾回收&lt;/a>&lt;/li>
&lt;/ul>
&lt;h2 id="問題背景">問題背景&lt;/h2>
&lt;h3 id="現有設計">現有設計&lt;/h3>
&lt;p>&lt;code>config_loader.py&lt;/code> 使用全域字典作為快取，這是一個常見的設計模式：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="c1"># Global cache variables&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">&lt;span class="n">_agents_config_cache&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">Optional&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="nb">dict&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="kc">None&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl">&lt;span class="n">_quality_rules_cache&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">Optional&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="nb">dict&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="kc">None&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl">&lt;span class="k">def&lt;/span> &lt;span class="nf">load_agents_config&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="o">-&amp;gt;&lt;/span> &lt;span class="nb">dict&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl"> &lt;span class="s2">&amp;#34;&amp;#34;&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl">&lt;span class="s2"> 載入代理人配置
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">&lt;span class="s2">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">&lt;span class="s2"> 使用模組層級變數作為快取，避免重複讀取檔案。
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">&lt;span class="s2"> &amp;#34;&amp;#34;&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl"> &lt;span class="k">global&lt;/span> &lt;span class="n">_agents_config_cache&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="n">_agents_config_cache&lt;/span> &lt;span class="ow">is&lt;/span> &lt;span class="kc">None&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">13&lt;/span>&lt;span class="cl"> &lt;span class="k">try&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">14&lt;/span>&lt;span class="cl"> &lt;span class="n">_agents_config_cache&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">load_config&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;agents&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">15&lt;/span>&lt;span class="cl"> &lt;span class="k">except&lt;/span> &lt;span class="ne">FileNotFoundError&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">16&lt;/span>&lt;span class="cl"> &lt;span class="n">_agents_config_cache&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">_get_default_agents_config&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">17&lt;/span>&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="n">_agents_config_cache&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">18&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">19&lt;/span>&lt;span class="cl">&lt;span class="k">def&lt;/span> &lt;span class="nf">clear_config_cache&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="o">-&amp;gt;&lt;/span> &lt;span class="kc">None&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">20&lt;/span>&lt;span class="cl"> &lt;span class="s2">&amp;#34;&amp;#34;&amp;#34;清除配置快取（用於測試或配置熱更新）&amp;#34;&amp;#34;&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">21&lt;/span>&lt;span class="cl"> &lt;span class="k">global&lt;/span> &lt;span class="n">_agents_config_cache&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">_quality_rules_cache&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">22&lt;/span>&lt;span class="cl"> &lt;span class="n">_agents_config_cache&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="kc">None&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">23&lt;/span>&lt;span class="cl"> &lt;span class="n">_quality_rules_cache&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="kc">None&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>這種設計簡單直觀，但當系統需要快取更複雜的物件時，會遇到記憶體問題。&lt;/p>
&lt;h3 id="記憶體問題">記憶體問題&lt;/h3>
&lt;p>當快取大量物件時：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Python 字典有額外開銷&lt;/strong>：每個字典需要維護 hash table、keys、values&lt;/li>
&lt;li>&lt;strong>物件的 &lt;code>__dict__&lt;/code> 佔用記憶體&lt;/strong>：每個實例都有自己的屬性字典&lt;/li>
&lt;li>&lt;strong>快取可能導致記憶體洩漏&lt;/strong>：強引用阻止物件被回收&lt;/li>
&lt;/ul>
&lt;p>讓我們用一個更複雜的快取場景來說明問題：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="kn">import&lt;/span> &lt;span class="nn">sys&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl">&lt;span class="k">class&lt;/span> &lt;span class="nc">ConfigItem&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl"> &lt;span class="s2">&amp;#34;&amp;#34;&amp;#34;配置項目，模擬複雜的快取物件&amp;#34;&amp;#34;&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl"> &lt;span class="k">def&lt;/span> &lt;span class="fm">__init__&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="bp">self&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">key&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="nb">str&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">value&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="nb">str&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">metadata&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="nb">dict&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl"> &lt;span class="bp">self&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">key&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">key&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl"> &lt;span class="bp">self&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">value&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">value&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl"> &lt;span class="bp">self&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">metadata&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">metadata&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl"> &lt;span class="bp">self&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">access_count&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="mi">0&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl"> &lt;span class="bp">self&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">last_accessed&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="kc">None&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl">&lt;span class="c1"># Create a config item and measure memory&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">13&lt;/span>&lt;span class="cl">&lt;span class="n">item&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">ConfigItem&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;database.host&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;localhost&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">{&lt;/span>&lt;span class="s2">&amp;#34;type&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;string&amp;#34;&lt;/span>&lt;span class="p">})&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">14&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">15&lt;/span>&lt;span class="cl">&lt;span class="c1"># Object size&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">16&lt;/span>&lt;span class="cl">&lt;span class="nb">print&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="sa">f&lt;/span>&lt;span class="s2">&amp;#34;ConfigItem size: &lt;/span>&lt;span class="si">{&lt;/span>&lt;span class="n">sys&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">getsizeof&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">item&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2"> bytes&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">17&lt;/span>&lt;span class="cl">&lt;span class="c1"># ConfigItem size: 48 bytes&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">18&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">19&lt;/span>&lt;span class="cl">&lt;span class="c1"># But the real cost is in __dict__&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">20&lt;/span>&lt;span class="cl">&lt;span class="nb">print&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="sa">f&lt;/span>&lt;span class="s2">&amp;#34;__dict__ size: &lt;/span>&lt;span class="si">{&lt;/span>&lt;span class="n">sys&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">getsizeof&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">item&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="vm">__dict__&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2"> bytes&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">21&lt;/span>&lt;span class="cl">&lt;span class="c1"># __dict__ size: 184 bytes&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>當快取數萬個這樣的物件時，記憶體開銷會非常可觀。&lt;/p>
&lt;hr>
&lt;h2 id="進階解決方案">進階解決方案&lt;/h2>
&lt;h3 id="優化目標">優化目標&lt;/h3>
&lt;ol>
&lt;li>減少每個物件的記憶體佔用&lt;/li>
&lt;li>避免快取導致的記憶體洩漏&lt;/li>
&lt;li>保持 API 不變&lt;/li>
&lt;/ol>
&lt;h3 id="實作步驟">實作步驟&lt;/h3>
&lt;h4 id="步驟-1使用-__slots__-減少物件大小">步驟 1：使用 &lt;code>__slots__&lt;/code> 減少物件大小&lt;/h4>
&lt;p>&lt;code>__slots__&lt;/code> 告訴 Python 這個類別只會有哪些屬性，讓直譯器可以用更緊湊的方式儲存資料：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="kn">import&lt;/span> &lt;span class="nn">sys&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl">&lt;span class="k">class&lt;/span> &lt;span class="nc">ConfigItemWithoutSlots&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl"> &lt;span class="s2">&amp;#34;&amp;#34;&amp;#34;標準類別，使用 __dict__&amp;#34;&amp;#34;&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl"> &lt;span class="k">def&lt;/span> &lt;span class="fm">__init__&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="bp">self&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">key&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="nb">str&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">value&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="nb">str&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">metadata&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="nb">dict&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl"> &lt;span class="bp">self&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">key&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">key&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl"> &lt;span class="bp">self&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">value&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">value&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl"> &lt;span class="bp">self&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">metadata&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">metadata&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl"> &lt;span class="bp">self&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">access_count&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="mi">0&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl"> &lt;span class="bp">self&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">last_accessed&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="kc">None&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl">&lt;span class="k">class&lt;/span> &lt;span class="nc">ConfigItemWithSlots&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">13&lt;/span>&lt;span class="cl"> &lt;span class="s2">&amp;#34;&amp;#34;&amp;#34;使用 __slots__ 優化記憶體&amp;#34;&amp;#34;&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">14&lt;/span>&lt;span class="cl"> &lt;span class="vm">__slots__&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;key&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;value&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;metadata&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;access_count&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;last_accessed&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">15&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">16&lt;/span>&lt;span class="cl"> &lt;span class="k">def&lt;/span> &lt;span class="fm">__init__&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="bp">self&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">key&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="nb">str&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">value&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="nb">str&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">metadata&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="nb">dict&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">17&lt;/span>&lt;span class="cl"> &lt;span class="bp">self&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">key&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">key&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">18&lt;/span>&lt;span class="cl"> &lt;span class="bp">self&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">value&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">value&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">19&lt;/span>&lt;span class="cl"> &lt;span class="bp">self&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">metadata&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">metadata&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">20&lt;/span>&lt;span class="cl"> &lt;span class="bp">self&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">access_count&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="mi">0&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">21&lt;/span>&lt;span class="cl"> &lt;span class="bp">self&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">last_accessed&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="kc">None&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">22&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">23&lt;/span>&lt;span class="cl">&lt;span class="c1"># Compare memory usage&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">24&lt;/span>&lt;span class="cl">&lt;span class="n">item_without&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">ConfigItemWithoutSlots&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;db.host&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;localhost&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">{&lt;/span>&lt;span class="s2">&amp;#34;type&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;str&amp;#34;&lt;/span>&lt;span class="p">})&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">25&lt;/span>&lt;span class="cl">&lt;span class="n">item_with&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">ConfigItemWithSlots&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;db.host&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;localhost&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">{&lt;/span>&lt;span class="s2">&amp;#34;type&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;str&amp;#34;&lt;/span>&lt;span class="p">})&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">26&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">27&lt;/span>&lt;span class="cl">&lt;span class="nb">print&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="sa">f&lt;/span>&lt;span class="s2">&amp;#34;Without __slots__: &lt;/span>&lt;span class="si">{&lt;/span>&lt;span class="n">sys&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">getsizeof&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">item_without&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2"> bytes&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">28&lt;/span>&lt;span class="cl">&lt;span class="nb">print&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="sa">f&lt;/span>&lt;span class="s2">&amp;#34;With __slots__: &lt;/span>&lt;span class="si">{&lt;/span>&lt;span class="n">sys&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">getsizeof&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">item_with&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2"> bytes&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">29&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">30&lt;/span>&lt;span class="cl">&lt;span class="c1"># The real difference is __dict__&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">31&lt;/span>&lt;span class="cl">&lt;span class="nb">print&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="sa">f&lt;/span>&lt;span class="s2">&amp;#34;__dict__ overhead: &lt;/span>&lt;span class="si">{&lt;/span>&lt;span class="n">sys&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">getsizeof&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">item_without&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="vm">__dict__&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2"> bytes&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">32&lt;/span>&lt;span class="cl">&lt;span class="c1"># item_with has no __dict__!&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">33&lt;/span>&lt;span class="cl">&lt;span class="k">try&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">34&lt;/span>&lt;span class="cl"> &lt;span class="n">item_with&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="vm">__dict__&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">35&lt;/span>&lt;span class="cl">&lt;span class="k">except&lt;/span> &lt;span class="ne">AttributeError&lt;/span> &lt;span class="k">as&lt;/span> &lt;span class="n">e&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">36&lt;/span>&lt;span class="cl"> &lt;span class="nb">print&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="sa">f&lt;/span>&lt;span class="s2">&amp;#34;No __dict__: &lt;/span>&lt;span class="si">{&lt;/span>&lt;span class="n">e&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h5 id="記憶體結構比較">記憶體結構比較&lt;/h5>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">沒有 __slots__:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">┌──────────────────────────────────┐
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl">│ PyObject header (16 B) │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl">│ __dict__ 指標 (8 B) │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl">│ __weakref__ 指標 (8 B) │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">│ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl">│ __dict__ (separate object): │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">│ - hash table (64 B) │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">│ - keys array (40 B) │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">│ - values array (40 B) │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl">│ - key strings (~80 B) │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl">│ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">13&lt;/span>&lt;span class="cl">│ Total: ~256 bytes per object │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">14&lt;/span>&lt;span class="cl">└──────────────────────────────────┘
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">15&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">16&lt;/span>&lt;span class="cl">有 __slots__:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">17&lt;/span>&lt;span class="cl">┌──────────────────────────────────┐
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">18&lt;/span>&lt;span class="cl">│ PyObject header (16 B) │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">19&lt;/span>&lt;span class="cl">│ key slot (8 B) │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">20&lt;/span>&lt;span class="cl">│ value slot (8 B) │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">21&lt;/span>&lt;span class="cl">│ metadata slot (8 B) │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">22&lt;/span>&lt;span class="cl">│ access_count slot (8 B) │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">23&lt;/span>&lt;span class="cl">│ last_accessed slot (8 B) │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">24&lt;/span>&lt;span class="cl">│ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">25&lt;/span>&lt;span class="cl">│ Total: ~56 bytes per object │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">26&lt;/span>&lt;span class="cl">└──────────────────────────────────┘&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h5 id="大量物件的記憶體節省">大量物件的記憶體節省&lt;/h5>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="kn">import&lt;/span> &lt;span class="nn">sys&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">&lt;span class="kn">import&lt;/span> &lt;span class="nn">tracemalloc&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl">&lt;span class="k">def&lt;/span> &lt;span class="nf">measure_memory&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="bp">cls&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">count&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="mi">10000&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl"> &lt;span class="s2">&amp;#34;&amp;#34;&amp;#34;Measure memory for creating multiple objects&amp;#34;&amp;#34;&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl"> &lt;span class="n">tracemalloc&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">start&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl"> &lt;span class="n">objects&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">[&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl"> &lt;span class="bp">cls&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="sa">f&lt;/span>&lt;span class="s2">&amp;#34;key_&lt;/span>&lt;span class="si">{&lt;/span>&lt;span class="n">i&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="sa">f&lt;/span>&lt;span class="s2">&amp;#34;value_&lt;/span>&lt;span class="si">{&lt;/span>&lt;span class="n">i&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">{&lt;/span>&lt;span class="s2">&amp;#34;index&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">i&lt;/span>&lt;span class="p">})&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl"> &lt;span class="k">for&lt;/span> &lt;span class="n">i&lt;/span> &lt;span class="ow">in&lt;/span> &lt;span class="nb">range&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">count&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl"> &lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">13&lt;/span>&lt;span class="cl"> &lt;span class="n">current&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">peak&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">tracemalloc&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">get_traced_memory&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">14&lt;/span>&lt;span class="cl"> &lt;span class="n">tracemalloc&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">stop&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">15&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">16&lt;/span>&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="n">current&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">peak&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">objects&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">17&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">18&lt;/span>&lt;span class="cl">&lt;span class="c1"># Measure both classes&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">19&lt;/span>&lt;span class="cl">&lt;span class="n">mem_without&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">peak_without&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">_&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">measure_memory&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">ConfigItemWithoutSlots&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">20&lt;/span>&lt;span class="cl">&lt;span class="n">mem_with&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">peak_with&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">_&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">measure_memory&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">ConfigItemWithSlots&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">21&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">22&lt;/span>&lt;span class="cl">&lt;span class="nb">print&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="sa">f&lt;/span>&lt;span class="s2">&amp;#34;Without __slots__: &lt;/span>&lt;span class="si">{&lt;/span>&lt;span class="n">mem_without&lt;/span> &lt;span class="o">/&lt;/span> &lt;span class="mi">1024&lt;/span> &lt;span class="o">/&lt;/span> &lt;span class="mi">1024&lt;/span>&lt;span class="si">:&lt;/span>&lt;span class="s2">.2f&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2"> MB&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">23&lt;/span>&lt;span class="cl">&lt;span class="nb">print&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="sa">f&lt;/span>&lt;span class="s2">&amp;#34;With __slots__: &lt;/span>&lt;span class="si">{&lt;/span>&lt;span class="n">mem_with&lt;/span> &lt;span class="o">/&lt;/span> &lt;span class="mi">1024&lt;/span> &lt;span class="o">/&lt;/span> &lt;span class="mi">1024&lt;/span>&lt;span class="si">:&lt;/span>&lt;span class="s2">.2f&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2"> MB&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">24&lt;/span>&lt;span class="cl">&lt;span class="nb">print&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="sa">f&lt;/span>&lt;span class="s2">&amp;#34;Savings: &lt;/span>&lt;span class="si">{&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">mem_without&lt;/span> &lt;span class="o">-&lt;/span> &lt;span class="n">mem_with&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">/&lt;/span> &lt;span class="mi">1024&lt;/span> &lt;span class="o">/&lt;/span> &lt;span class="mi">1024&lt;/span>&lt;span class="si">:&lt;/span>&lt;span class="s2">.2f&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2"> MB&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">25&lt;/span>&lt;span class="cl">&lt;span class="nb">print&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="sa">f&lt;/span>&lt;span class="s2">&amp;#34;Ratio: &lt;/span>&lt;span class="si">{&lt;/span>&lt;span class="n">mem_without&lt;/span> &lt;span class="o">/&lt;/span> &lt;span class="n">mem_with&lt;/span>&lt;span class="si">:&lt;/span>&lt;span class="s2">.1f&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2">x&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">26&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">27&lt;/span>&lt;span class="cl">&lt;span class="c1"># Typical output:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">28&lt;/span>&lt;span class="cl">&lt;span class="c1"># Without __slots__: 3.82 MB&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">29&lt;/span>&lt;span class="cl">&lt;span class="c1"># With __slots__: 1.15 MB&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">30&lt;/span>&lt;span class="cl">&lt;span class="c1"># Savings: 2.67 MB&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">31&lt;/span>&lt;span class="cl">&lt;span class="c1"># Ratio: 3.3x&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h4 id="步驟-2使用-weakref-避免強引用">步驟 2：使用 weakref 避免強引用&lt;/h4>
&lt;p>&lt;code>weakref&lt;/code> 讓我們可以引用物件，但不阻止它被垃圾回收：&lt;/p></description><content:encoded><![CDATA[<p>本案例基於 <code>.claude/lib/config_loader.py</code> 的實際程式碼，展示如何用 <code>__slots__</code> 和 <code>weakref</code> 優化記憶體使用。</p>
<h2 id="先備知識">先備知識</h2>
<ul>
<li><a href="/blog/python-advanced/04-cpython-internals/" data-link-title="模組四：CPython 內部機制" data-link-desc="深入 CPython 直譯器，理解 Python 如何運作">模組四：CPython 內部機制</a></li>
<li><a href="/blog/python-advanced/04-cpython-internals/memory-gc/" data-link-title="3.2 記憶體管理與垃圾回收" data-link-desc="理解 Python 的記憶體管理機制">4.2 記憶體管理與垃圾回收</a></li>
</ul>
<h2 id="問題背景">問題背景</h2>
<h3 id="現有設計">現有設計</h3>
<p><code>config_loader.py</code> 使用全域字典作為快取，這是一個常見的設計模式：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1"># Global cache variables</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="n">_agents_config_cache</span><span class="p">:</span> <span class="n">Optional</span><span class="p">[</span><span class="nb">dict</span><span class="p">]</span> <span class="o">=</span> <span class="kc">None</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="n">_quality_rules_cache</span><span class="p">:</span> <span class="n">Optional</span><span class="p">[</span><span class="nb">dict</span><span class="p">]</span> <span class="o">=</span> <span class="kc">None</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">
</span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="k">def</span> <span class="nf">load_agents_config</span><span class="p">()</span> <span class="o">-&gt;</span> <span class="nb">dict</span><span class="p">:</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">    <span class="s2">&#34;&#34;&#34;
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="s2">    載入代理人配置
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="s2">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="s2">    使用模組層級變數作為快取，避免重複讀取檔案。
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="s2">    &#34;&#34;&#34;</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">    <span class="k">global</span> <span class="n">_agents_config_cache</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">    <span class="k">if</span> <span class="n">_agents_config_cache</span> <span class="ow">is</span> <span class="kc">None</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">        <span class="k">try</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">            <span class="n">_agents_config_cache</span> <span class="o">=</span> <span class="n">load_config</span><span class="p">(</span><span class="s2">&#34;agents&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">        <span class="k">except</span> <span class="ne">FileNotFoundError</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">            <span class="n">_agents_config_cache</span> <span class="o">=</span> <span class="n">_get_default_agents_config</span><span class="p">()</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl">    <span class="k">return</span> <span class="n">_agents_config_cache</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl">
</span></span><span class="line"><span class="ln">19</span><span class="cl"><span class="k">def</span> <span class="nf">clear_config_cache</span><span class="p">()</span> <span class="o">-&gt;</span> <span class="kc">None</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">20</span><span class="cl">    <span class="s2">&#34;&#34;&#34;清除配置快取（用於測試或配置熱更新）&#34;&#34;&#34;</span>
</span></span><span class="line"><span class="ln">21</span><span class="cl">    <span class="k">global</span> <span class="n">_agents_config_cache</span><span class="p">,</span> <span class="n">_quality_rules_cache</span>
</span></span><span class="line"><span class="ln">22</span><span class="cl">    <span class="n">_agents_config_cache</span> <span class="o">=</span> <span class="kc">None</span>
</span></span><span class="line"><span class="ln">23</span><span class="cl">    <span class="n">_quality_rules_cache</span> <span class="o">=</span> <span class="kc">None</span></span></span></code></pre></div><p>這種設計簡單直觀，但當系統需要快取更複雜的物件時，會遇到記憶體問題。</p>
<h3 id="記憶體問題">記憶體問題</h3>
<p>當快取大量物件時：</p>
<ul>
<li><strong>Python 字典有額外開銷</strong>：每個字典需要維護 hash table、keys、values</li>
<li><strong>物件的 <code>__dict__</code> 佔用記憶體</strong>：每個實例都有自己的屬性字典</li>
<li><strong>快取可能導致記憶體洩漏</strong>：強引用阻止物件被回收</li>
</ul>
<p>讓我們用一個更複雜的快取場景來說明問題：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="kn">import</span> <span class="nn">sys</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="k">class</span> <span class="nc">ConfigItem</span><span class="p">:</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">    <span class="s2">&#34;&#34;&#34;配置項目，模擬複雜的快取物件&#34;&#34;&#34;</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">    <span class="k">def</span> <span class="fm">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">key</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span> <span class="n">value</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span> <span class="n">metadata</span><span class="p">:</span> <span class="nb">dict</span><span class="p">):</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">key</span> <span class="o">=</span> <span class="n">key</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">value</span> <span class="o">=</span> <span class="n">value</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">metadata</span> <span class="o">=</span> <span class="n">metadata</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">access_count</span> <span class="o">=</span> <span class="mi">0</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">last_accessed</span> <span class="o">=</span> <span class="kc">None</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">
</span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="c1"># Create a config item and measure memory</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="n">item</span> <span class="o">=</span> <span class="n">ConfigItem</span><span class="p">(</span><span class="s2">&#34;database.host&#34;</span><span class="p">,</span> <span class="s2">&#34;localhost&#34;</span><span class="p">,</span> <span class="p">{</span><span class="s2">&#34;type&#34;</span><span class="p">:</span> <span class="s2">&#34;string&#34;</span><span class="p">})</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">
</span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="c1"># Object size</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="nb">print</span><span class="p">(</span><span class="sa">f</span><span class="s2">&#34;ConfigItem size: </span><span class="si">{</span><span class="n">sys</span><span class="o">.</span><span class="n">getsizeof</span><span class="p">(</span><span class="n">item</span><span class="p">)</span><span class="si">}</span><span class="s2"> bytes&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="c1"># ConfigItem size: 48 bytes</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl">
</span></span><span class="line"><span class="ln">19</span><span class="cl"><span class="c1"># But the real cost is in __dict__</span>
</span></span><span class="line"><span class="ln">20</span><span class="cl"><span class="nb">print</span><span class="p">(</span><span class="sa">f</span><span class="s2">&#34;__dict__ size: </span><span class="si">{</span><span class="n">sys</span><span class="o">.</span><span class="n">getsizeof</span><span class="p">(</span><span class="n">item</span><span class="o">.</span><span class="vm">__dict__</span><span class="p">)</span><span class="si">}</span><span class="s2"> bytes&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">21</span><span class="cl"><span class="c1"># __dict__ size: 184 bytes</span></span></span></code></pre></div><p>當快取數萬個這樣的物件時，記憶體開銷會非常可觀。</p>
<hr>
<h2 id="進階解決方案">進階解決方案</h2>
<h3 id="優化目標">優化目標</h3>
<ol>
<li>減少每個物件的記憶體佔用</li>
<li>避免快取導致的記憶體洩漏</li>
<li>保持 API 不變</li>
</ol>
<h3 id="實作步驟">實作步驟</h3>
<h4 id="步驟-1使用-__slots__-減少物件大小">步驟 1：使用 <code>__slots__</code> 減少物件大小</h4>
<p><code>__slots__</code> 告訴 Python 這個類別只會有哪些屬性，讓直譯器可以用更緊湊的方式儲存資料：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="kn">import</span> <span class="nn">sys</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="k">class</span> <span class="nc">ConfigItemWithoutSlots</span><span class="p">:</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">    <span class="s2">&#34;&#34;&#34;標準類別，使用 __dict__&#34;&#34;&#34;</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">    <span class="k">def</span> <span class="fm">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">key</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span> <span class="n">value</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span> <span class="n">metadata</span><span class="p">:</span> <span class="nb">dict</span><span class="p">):</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">key</span> <span class="o">=</span> <span class="n">key</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">value</span> <span class="o">=</span> <span class="n">value</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">metadata</span> <span class="o">=</span> <span class="n">metadata</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">access_count</span> <span class="o">=</span> <span class="mi">0</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">last_accessed</span> <span class="o">=</span> <span class="kc">None</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">
</span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="k">class</span> <span class="nc">ConfigItemWithSlots</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">    <span class="s2">&#34;&#34;&#34;使用 __slots__ 優化記憶體&#34;&#34;&#34;</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">    <span class="vm">__slots__</span> <span class="o">=</span> <span class="p">[</span><span class="s1">&#39;key&#39;</span><span class="p">,</span> <span class="s1">&#39;value&#39;</span><span class="p">,</span> <span class="s1">&#39;metadata&#39;</span><span class="p">,</span> <span class="s1">&#39;access_count&#39;</span><span class="p">,</span> <span class="s1">&#39;last_accessed&#39;</span><span class="p">]</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">
</span></span><span class="line"><span class="ln">16</span><span class="cl">    <span class="k">def</span> <span class="fm">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">key</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span> <span class="n">value</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span> <span class="n">metadata</span><span class="p">:</span> <span class="nb">dict</span><span class="p">):</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">key</span> <span class="o">=</span> <span class="n">key</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">value</span> <span class="o">=</span> <span class="n">value</span>
</span></span><span class="line"><span class="ln">19</span><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">metadata</span> <span class="o">=</span> <span class="n">metadata</span>
</span></span><span class="line"><span class="ln">20</span><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">access_count</span> <span class="o">=</span> <span class="mi">0</span>
</span></span><span class="line"><span class="ln">21</span><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">last_accessed</span> <span class="o">=</span> <span class="kc">None</span>
</span></span><span class="line"><span class="ln">22</span><span class="cl">
</span></span><span class="line"><span class="ln">23</span><span class="cl"><span class="c1"># Compare memory usage</span>
</span></span><span class="line"><span class="ln">24</span><span class="cl"><span class="n">item_without</span> <span class="o">=</span> <span class="n">ConfigItemWithoutSlots</span><span class="p">(</span><span class="s2">&#34;db.host&#34;</span><span class="p">,</span> <span class="s2">&#34;localhost&#34;</span><span class="p">,</span> <span class="p">{</span><span class="s2">&#34;type&#34;</span><span class="p">:</span> <span class="s2">&#34;str&#34;</span><span class="p">})</span>
</span></span><span class="line"><span class="ln">25</span><span class="cl"><span class="n">item_with</span> <span class="o">=</span> <span class="n">ConfigItemWithSlots</span><span class="p">(</span><span class="s2">&#34;db.host&#34;</span><span class="p">,</span> <span class="s2">&#34;localhost&#34;</span><span class="p">,</span> <span class="p">{</span><span class="s2">&#34;type&#34;</span><span class="p">:</span> <span class="s2">&#34;str&#34;</span><span class="p">})</span>
</span></span><span class="line"><span class="ln">26</span><span class="cl">
</span></span><span class="line"><span class="ln">27</span><span class="cl"><span class="nb">print</span><span class="p">(</span><span class="sa">f</span><span class="s2">&#34;Without __slots__: </span><span class="si">{</span><span class="n">sys</span><span class="o">.</span><span class="n">getsizeof</span><span class="p">(</span><span class="n">item_without</span><span class="p">)</span><span class="si">}</span><span class="s2"> bytes&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">28</span><span class="cl"><span class="nb">print</span><span class="p">(</span><span class="sa">f</span><span class="s2">&#34;With __slots__:    </span><span class="si">{</span><span class="n">sys</span><span class="o">.</span><span class="n">getsizeof</span><span class="p">(</span><span class="n">item_with</span><span class="p">)</span><span class="si">}</span><span class="s2"> bytes&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">29</span><span class="cl">
</span></span><span class="line"><span class="ln">30</span><span class="cl"><span class="c1"># The real difference is __dict__</span>
</span></span><span class="line"><span class="ln">31</span><span class="cl"><span class="nb">print</span><span class="p">(</span><span class="sa">f</span><span class="s2">&#34;__dict__ overhead: </span><span class="si">{</span><span class="n">sys</span><span class="o">.</span><span class="n">getsizeof</span><span class="p">(</span><span class="n">item_without</span><span class="o">.</span><span class="vm">__dict__</span><span class="p">)</span><span class="si">}</span><span class="s2"> bytes&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">32</span><span class="cl"><span class="c1"># item_with has no __dict__!</span>
</span></span><span class="line"><span class="ln">33</span><span class="cl"><span class="k">try</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">34</span><span class="cl">    <span class="n">item_with</span><span class="o">.</span><span class="vm">__dict__</span>
</span></span><span class="line"><span class="ln">35</span><span class="cl"><span class="k">except</span> <span class="ne">AttributeError</span> <span class="k">as</span> <span class="n">e</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">36</span><span class="cl">    <span class="nb">print</span><span class="p">(</span><span class="sa">f</span><span class="s2">&#34;No __dict__: </span><span class="si">{</span><span class="n">e</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">)</span></span></span></code></pre></div><h5 id="記憶體結構比較">記憶體結構比較</h5>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln"> 1</span><span class="cl">沒有 __slots__:
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">┌──────────────────────────────────┐
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">│ PyObject header         (16 B)   │
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">│ __dict__ 指標            (8 B)   │
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">│ __weakref__ 指標         (8 B)   │
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">│                                  │
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">│ __dict__ (separate object):      │
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">│   - hash table          (64 B)   │
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">│   - keys array          (40 B)   │
</span></span><span class="line"><span class="ln">10</span><span class="cl">│   - values array        (40 B)   │
</span></span><span class="line"><span class="ln">11</span><span class="cl">│   - key strings        (~80 B)   │
</span></span><span class="line"><span class="ln">12</span><span class="cl">│                                  │
</span></span><span class="line"><span class="ln">13</span><span class="cl">│ Total: ~256 bytes per object     │
</span></span><span class="line"><span class="ln">14</span><span class="cl">└──────────────────────────────────┘
</span></span><span class="line"><span class="ln">15</span><span class="cl">
</span></span><span class="line"><span class="ln">16</span><span class="cl">有 __slots__:
</span></span><span class="line"><span class="ln">17</span><span class="cl">┌──────────────────────────────────┐
</span></span><span class="line"><span class="ln">18</span><span class="cl">│ PyObject header         (16 B)   │
</span></span><span class="line"><span class="ln">19</span><span class="cl">│ key slot                 (8 B)   │
</span></span><span class="line"><span class="ln">20</span><span class="cl">│ value slot               (8 B)   │
</span></span><span class="line"><span class="ln">21</span><span class="cl">│ metadata slot            (8 B)   │
</span></span><span class="line"><span class="ln">22</span><span class="cl">│ access_count slot        (8 B)   │
</span></span><span class="line"><span class="ln">23</span><span class="cl">│ last_accessed slot       (8 B)   │
</span></span><span class="line"><span class="ln">24</span><span class="cl">│                                  │
</span></span><span class="line"><span class="ln">25</span><span class="cl">│ Total: ~56 bytes per object      │
</span></span><span class="line"><span class="ln">26</span><span class="cl">└──────────────────────────────────┘</span></span></code></pre></div><h5 id="大量物件的記憶體節省">大量物件的記憶體節省</h5>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="kn">import</span> <span class="nn">sys</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="kn">import</span> <span class="nn">tracemalloc</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="k">def</span> <span class="nf">measure_memory</span><span class="p">(</span><span class="bp">cls</span><span class="p">,</span> <span class="n">count</span><span class="o">=</span><span class="mi">10000</span><span class="p">):</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">    <span class="s2">&#34;&#34;&#34;Measure memory for creating multiple objects&#34;&#34;&#34;</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">    <span class="n">tracemalloc</span><span class="o">.</span><span class="n">start</span><span class="p">()</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">    <span class="n">objects</span> <span class="o">=</span> <span class="p">[</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">        <span class="bp">cls</span><span class="p">(</span><span class="sa">f</span><span class="s2">&#34;key_</span><span class="si">{</span><span class="n">i</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">,</span> <span class="sa">f</span><span class="s2">&#34;value_</span><span class="si">{</span><span class="n">i</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">,</span> <span class="p">{</span><span class="s2">&#34;index&#34;</span><span class="p">:</span> <span class="n">i</span><span class="p">})</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">        <span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="n">count</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">    <span class="p">]</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">
</span></span><span class="line"><span class="ln">13</span><span class="cl">    <span class="n">current</span><span class="p">,</span> <span class="n">peak</span> <span class="o">=</span> <span class="n">tracemalloc</span><span class="o">.</span><span class="n">get_traced_memory</span><span class="p">()</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">    <span class="n">tracemalloc</span><span class="o">.</span><span class="n">stop</span><span class="p">()</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">
</span></span><span class="line"><span class="ln">16</span><span class="cl">    <span class="k">return</span> <span class="n">current</span><span class="p">,</span> <span class="n">peak</span><span class="p">,</span> <span class="n">objects</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl">
</span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="c1"># Measure both classes</span>
</span></span><span class="line"><span class="ln">19</span><span class="cl"><span class="n">mem_without</span><span class="p">,</span> <span class="n">peak_without</span><span class="p">,</span> <span class="n">_</span> <span class="o">=</span> <span class="n">measure_memory</span><span class="p">(</span><span class="n">ConfigItemWithoutSlots</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">20</span><span class="cl"><span class="n">mem_with</span><span class="p">,</span> <span class="n">peak_with</span><span class="p">,</span> <span class="n">_</span> <span class="o">=</span> <span class="n">measure_memory</span><span class="p">(</span><span class="n">ConfigItemWithSlots</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">21</span><span class="cl">
</span></span><span class="line"><span class="ln">22</span><span class="cl"><span class="nb">print</span><span class="p">(</span><span class="sa">f</span><span class="s2">&#34;Without __slots__: </span><span class="si">{</span><span class="n">mem_without</span> <span class="o">/</span> <span class="mi">1024</span> <span class="o">/</span> <span class="mi">1024</span><span class="si">:</span><span class="s2">.2f</span><span class="si">}</span><span class="s2"> MB&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">23</span><span class="cl"><span class="nb">print</span><span class="p">(</span><span class="sa">f</span><span class="s2">&#34;With __slots__:    </span><span class="si">{</span><span class="n">mem_with</span> <span class="o">/</span> <span class="mi">1024</span> <span class="o">/</span> <span class="mi">1024</span><span class="si">:</span><span class="s2">.2f</span><span class="si">}</span><span class="s2"> MB&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">24</span><span class="cl"><span class="nb">print</span><span class="p">(</span><span class="sa">f</span><span class="s2">&#34;Savings:           </span><span class="si">{</span><span class="p">(</span><span class="n">mem_without</span> <span class="o">-</span> <span class="n">mem_with</span><span class="p">)</span> <span class="o">/</span> <span class="mi">1024</span> <span class="o">/</span> <span class="mi">1024</span><span class="si">:</span><span class="s2">.2f</span><span class="si">}</span><span class="s2"> MB&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">25</span><span class="cl"><span class="nb">print</span><span class="p">(</span><span class="sa">f</span><span class="s2">&#34;Ratio:             </span><span class="si">{</span><span class="n">mem_without</span> <span class="o">/</span> <span class="n">mem_with</span><span class="si">:</span><span class="s2">.1f</span><span class="si">}</span><span class="s2">x&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">26</span><span class="cl">
</span></span><span class="line"><span class="ln">27</span><span class="cl"><span class="c1"># Typical output:</span>
</span></span><span class="line"><span class="ln">28</span><span class="cl"><span class="c1"># Without __slots__: 3.82 MB</span>
</span></span><span class="line"><span class="ln">29</span><span class="cl"><span class="c1"># With __slots__:    1.15 MB</span>
</span></span><span class="line"><span class="ln">30</span><span class="cl"><span class="c1"># Savings:           2.67 MB</span>
</span></span><span class="line"><span class="ln">31</span><span class="cl"><span class="c1"># Ratio:             3.3x</span></span></span></code></pre></div><h4 id="步驟-2使用-weakref-避免強引用">步驟 2：使用 weakref 避免強引用</h4>
<p><code>weakref</code> 讓我們可以引用物件，但不阻止它被垃圾回收：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="kn">import</span> <span class="nn">weakref</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="k">class</span> <span class="nc">CacheableConfig</span><span class="p">:</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">    <span class="s2">&#34;&#34;&#34;可以被弱引用的配置物件&#34;&#34;&#34;</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">    <span class="vm">__slots__</span> <span class="o">=</span> <span class="p">[</span><span class="s1">&#39;key&#39;</span><span class="p">,</span> <span class="s1">&#39;value&#39;</span><span class="p">,</span> <span class="s1">&#39;_data&#39;</span><span class="p">,</span> <span class="s1">&#39;__weakref__&#39;</span><span class="p">]</span>  <span class="c1"># Note: __weakref__ slot</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">    <span class="k">def</span> <span class="fm">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">key</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span> <span class="n">value</span><span class="p">:</span> <span class="nb">str</span><span class="p">):</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">key</span> <span class="o">=</span> <span class="n">key</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">value</span> <span class="o">=</span> <span class="n">value</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">_data</span> <span class="o">=</span> <span class="kc">None</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">
</span></span><span class="line"><span class="ln">12</span><span class="cl">    <span class="k">def</span> <span class="fm">__repr__</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">        <span class="k">return</span> <span class="sa">f</span><span class="s2">&#34;CacheableConfig(</span><span class="si">{</span><span class="bp">self</span><span class="o">.</span><span class="n">key</span><span class="si">!r}</span><span class="s2">, </span><span class="si">{</span><span class="bp">self</span><span class="o">.</span><span class="n">value</span><span class="si">!r}</span><span class="s2">)&#34;</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">
</span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="c1"># Create object and weak reference</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="n">config</span> <span class="o">=</span> <span class="n">CacheableConfig</span><span class="p">(</span><span class="s2">&#34;app.name&#34;</span><span class="p">,</span> <span class="s2">&#34;MyApp&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="n">weak_ref</span> <span class="o">=</span> <span class="n">weakref</span><span class="o">.</span><span class="n">ref</span><span class="p">(</span><span class="n">config</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl">
</span></span><span class="line"><span class="ln">19</span><span class="cl"><span class="nb">print</span><span class="p">(</span><span class="sa">f</span><span class="s2">&#34;Object exists: </span><span class="si">{</span><span class="n">weak_ref</span><span class="p">()</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">20</span><span class="cl"><span class="c1"># Object exists: CacheableConfig(&#39;app.name&#39;, &#39;MyApp&#39;)</span>
</span></span><span class="line"><span class="ln">21</span><span class="cl">
</span></span><span class="line"><span class="ln">22</span><span class="cl"><span class="c1"># Delete the strong reference</span>
</span></span><span class="line"><span class="ln">23</span><span class="cl"><span class="k">del</span> <span class="n">config</span>
</span></span><span class="line"><span class="ln">24</span><span class="cl">
</span></span><span class="line"><span class="ln">25</span><span class="cl"><span class="nb">print</span><span class="p">(</span><span class="sa">f</span><span class="s2">&#34;After del: </span><span class="si">{</span><span class="n">weak_ref</span><span class="p">()</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">26</span><span class="cl"><span class="c1"># After del: None</span></span></span></code></pre></div><h5 id="使用-callback-追蹤物件回收">使用 callback 追蹤物件回收</h5>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="kn">import</span> <span class="nn">weakref</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="k">def</span> <span class="nf">on_finalize</span><span class="p">(</span><span class="n">ref</span><span class="p">):</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">    <span class="s2">&#34;&#34;&#34;Callback when object is garbage collected&#34;&#34;&#34;</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">    <span class="nb">print</span><span class="p">(</span><span class="sa">f</span><span class="s2">&#34;Object was garbage collected!&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="n">config</span> <span class="o">=</span> <span class="n">CacheableConfig</span><span class="p">(</span><span class="s2">&#34;db.port&#34;</span><span class="p">,</span> <span class="s2">&#34;5432&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="n">weak_ref</span> <span class="o">=</span> <span class="n">weakref</span><span class="o">.</span><span class="n">ref</span><span class="p">(</span><span class="n">config</span><span class="p">,</span> <span class="n">on_finalize</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="nb">print</span><span class="p">(</span><span class="s2">&#34;Deleting object...&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="k">del</span> <span class="n">config</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="c1"># Output: Object was garbage collected!</span></span></span></code></pre></div><h4 id="步驟-3使用-weakvaluedictionary">步驟 3：使用 WeakValueDictionary</h4>
<p><code>WeakValueDictionary</code> 是實作自動清理快取的利器：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="kn">import</span> <span class="nn">weakref</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="kn">from</span> <span class="nn">typing</span> <span class="kn">import</span> <span class="n">Callable</span><span class="p">,</span> <span class="n">TypeVar</span><span class="p">,</span> <span class="n">Generic</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="n">T</span> <span class="o">=</span> <span class="n">TypeVar</span><span class="p">(</span><span class="s1">&#39;T&#39;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="k">class</span> <span class="nc">WeakCache</span><span class="p">(</span><span class="n">Generic</span><span class="p">[</span><span class="n">T</span><span class="p">]):</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">    <span class="s2">&#34;&#34;&#34;
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="s2">    Auto-cleaning cache using weak references.
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="s2">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="s2">    Objects are automatically removed from cache when no strong
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="s2">    references exist outside the cache.
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="s2">    &#34;&#34;&#34;</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">
</span></span><span class="line"><span class="ln">14</span><span class="cl">    <span class="k">def</span> <span class="fm">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">_cache</span><span class="p">:</span> <span class="n">weakref</span><span class="o">.</span><span class="n">WeakValueDictionary</span><span class="p">[</span><span class="nb">str</span><span class="p">,</span> <span class="n">T</span><span class="p">]</span> <span class="o">=</span> <span class="p">(</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">            <span class="n">weakref</span><span class="o">.</span><span class="n">WeakValueDictionary</span><span class="p">()</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl">        <span class="p">)</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">_hits</span> <span class="o">=</span> <span class="mi">0</span>
</span></span><span class="line"><span class="ln">19</span><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">_misses</span> <span class="o">=</span> <span class="mi">0</span>
</span></span><span class="line"><span class="ln">20</span><span class="cl">
</span></span><span class="line"><span class="ln">21</span><span class="cl">    <span class="k">def</span> <span class="nf">get</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">key</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span> <span class="n">factory</span><span class="p">:</span> <span class="n">Callable</span><span class="p">[[],</span> <span class="n">T</span><span class="p">])</span> <span class="o">-&gt;</span> <span class="n">T</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">22</span><span class="cl">        <span class="s2">&#34;&#34;&#34;
</span></span></span><span class="line"><span class="ln">23</span><span class="cl"><span class="s2">        Get item from cache, creating it if necessary.
</span></span></span><span class="line"><span class="ln">24</span><span class="cl"><span class="s2">
</span></span></span><span class="line"><span class="ln">25</span><span class="cl"><span class="s2">        Args:
</span></span></span><span class="line"><span class="ln">26</span><span class="cl"><span class="s2">            key: Cache key
</span></span></span><span class="line"><span class="ln">27</span><span class="cl"><span class="s2">            factory: Function to create value if not cached
</span></span></span><span class="line"><span class="ln">28</span><span class="cl"><span class="s2">
</span></span></span><span class="line"><span class="ln">29</span><span class="cl"><span class="s2">        Returns:
</span></span></span><span class="line"><span class="ln">30</span><span class="cl"><span class="s2">            Cached or newly created value
</span></span></span><span class="line"><span class="ln">31</span><span class="cl"><span class="s2">        &#34;&#34;&#34;</span>
</span></span><span class="line"><span class="ln">32</span><span class="cl">        <span class="n">value</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_cache</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="n">key</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">33</span><span class="cl">        <span class="k">if</span> <span class="n">value</span> <span class="ow">is</span> <span class="ow">not</span> <span class="kc">None</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">34</span><span class="cl">            <span class="bp">self</span><span class="o">.</span><span class="n">_hits</span> <span class="o">+=</span> <span class="mi">1</span>
</span></span><span class="line"><span class="ln">35</span><span class="cl">            <span class="k">return</span> <span class="n">value</span>
</span></span><span class="line"><span class="ln">36</span><span class="cl">
</span></span><span class="line"><span class="ln">37</span><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">_misses</span> <span class="o">+=</span> <span class="mi">1</span>
</span></span><span class="line"><span class="ln">38</span><span class="cl">        <span class="n">value</span> <span class="o">=</span> <span class="n">factory</span><span class="p">()</span>
</span></span><span class="line"><span class="ln">39</span><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">_cache</span><span class="p">[</span><span class="n">key</span><span class="p">]</span> <span class="o">=</span> <span class="n">value</span>
</span></span><span class="line"><span class="ln">40</span><span class="cl">        <span class="k">return</span> <span class="n">value</span>
</span></span><span class="line"><span class="ln">41</span><span class="cl">
</span></span><span class="line"><span class="ln">42</span><span class="cl">    <span class="k">def</span> <span class="fm">__len__</span><span class="p">(</span><span class="bp">self</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">int</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">43</span><span class="cl">        <span class="k">return</span> <span class="nb">len</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">_cache</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">44</span><span class="cl">
</span></span><span class="line"><span class="ln">45</span><span class="cl">    <span class="k">def</span> <span class="nf">stats</span><span class="p">(</span><span class="bp">self</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">dict</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">46</span><span class="cl">        <span class="s2">&#34;&#34;&#34;Return cache statistics&#34;&#34;&#34;</span>
</span></span><span class="line"><span class="ln">47</span><span class="cl">        <span class="n">total</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_hits</span> <span class="o">+</span> <span class="bp">self</span><span class="o">.</span><span class="n">_misses</span>
</span></span><span class="line"><span class="ln">48</span><span class="cl">        <span class="n">hit_rate</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_hits</span> <span class="o">/</span> <span class="n">total</span> <span class="k">if</span> <span class="n">total</span> <span class="o">&gt;</span> <span class="mi">0</span> <span class="k">else</span> <span class="mi">0</span>
</span></span><span class="line"><span class="ln">49</span><span class="cl">        <span class="k">return</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">50</span><span class="cl">            <span class="s2">&#34;hits&#34;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">_hits</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">51</span><span class="cl">            <span class="s2">&#34;misses&#34;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">_misses</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">52</span><span class="cl">            <span class="s2">&#34;hit_rate&#34;</span><span class="p">:</span> <span class="sa">f</span><span class="s2">&#34;</span><span class="si">{</span><span class="n">hit_rate</span><span class="si">:</span><span class="s2">.1%</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">53</span><span class="cl">            <span class="s2">&#34;size&#34;</span><span class="p">:</span> <span class="nb">len</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">_cache</span><span class="p">),</span>
</span></span><span class="line"><span class="ln">54</span><span class="cl">        <span class="p">}</span>
</span></span><span class="line"><span class="ln">55</span><span class="cl">
</span></span><span class="line"><span class="ln">56</span><span class="cl"><span class="c1"># Demo: automatic cleanup</span>
</span></span><span class="line"><span class="ln">57</span><span class="cl"><span class="n">cache</span> <span class="o">=</span> <span class="n">WeakCache</span><span class="p">[</span><span class="n">CacheableConfig</span><span class="p">]()</span>
</span></span><span class="line"><span class="ln">58</span><span class="cl">
</span></span><span class="line"><span class="ln">59</span><span class="cl"><span class="c1"># Create and cache object</span>
</span></span><span class="line"><span class="ln">60</span><span class="cl"><span class="n">config1</span> <span class="o">=</span> <span class="n">cache</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s2">&#34;app.name&#34;</span><span class="p">,</span> <span class="k">lambda</span><span class="p">:</span> <span class="n">CacheableConfig</span><span class="p">(</span><span class="s2">&#34;app.name&#34;</span><span class="p">,</span> <span class="s2">&#34;MyApp&#34;</span><span class="p">))</span>
</span></span><span class="line"><span class="ln">61</span><span class="cl"><span class="n">config2</span> <span class="o">=</span> <span class="n">cache</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s2">&#34;app.name&#34;</span><span class="p">,</span> <span class="k">lambda</span><span class="p">:</span> <span class="n">CacheableConfig</span><span class="p">(</span><span class="s2">&#34;app.name&#34;</span><span class="p">,</span> <span class="s2">&#34;MyApp&#34;</span><span class="p">))</span>  <span class="c1"># Cache hit</span>
</span></span><span class="line"><span class="ln">62</span><span class="cl">
</span></span><span class="line"><span class="ln">63</span><span class="cl"><span class="nb">print</span><span class="p">(</span><span class="sa">f</span><span class="s2">&#34;Cache size: </span><span class="si">{</span><span class="nb">len</span><span class="p">(</span><span class="n">cache</span><span class="p">)</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">)</span>  <span class="c1"># 1</span>
</span></span><span class="line"><span class="ln">64</span><span class="cl"><span class="nb">print</span><span class="p">(</span><span class="sa">f</span><span class="s2">&#34;Same object: </span><span class="si">{</span><span class="n">config1</span> <span class="ow">is</span> <span class="n">config2</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">)</span>  <span class="c1"># True</span>
</span></span><span class="line"><span class="ln">65</span><span class="cl"><span class="nb">print</span><span class="p">(</span><span class="sa">f</span><span class="s2">&#34;Stats: </span><span class="si">{</span><span class="n">cache</span><span class="o">.</span><span class="n">stats</span><span class="p">()</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">)</span>  <span class="c1"># hits=1, misses=1</span>
</span></span><span class="line"><span class="ln">66</span><span class="cl">
</span></span><span class="line"><span class="ln">67</span><span class="cl"><span class="c1"># Delete strong reference</span>
</span></span><span class="line"><span class="ln">68</span><span class="cl"><span class="k">del</span> <span class="n">config1</span>
</span></span><span class="line"><span class="ln">69</span><span class="cl"><span class="k">del</span> <span class="n">config2</span>
</span></span><span class="line"><span class="ln">70</span><span class="cl">
</span></span><span class="line"><span class="ln">71</span><span class="cl"><span class="c1"># Object is garbage collected, cache is auto-cleaned</span>
</span></span><span class="line"><span class="ln">72</span><span class="cl"><span class="kn">import</span> <span class="nn">gc</span>
</span></span><span class="line"><span class="ln">73</span><span class="cl"><span class="n">gc</span><span class="o">.</span><span class="n">collect</span><span class="p">()</span>
</span></span><span class="line"><span class="ln">74</span><span class="cl">
</span></span><span class="line"><span class="ln">75</span><span class="cl"><span class="nb">print</span><span class="p">(</span><span class="sa">f</span><span class="s2">&#34;Cache size after cleanup: </span><span class="si">{</span><span class="nb">len</span><span class="p">(</span><span class="n">cache</span><span class="p">)</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">)</span>  <span class="c1"># 0</span></span></span></code></pre></div><h4 id="步驟-4測量記憶體使用">步驟 4：測量記憶體使用</h4>
<p>使用 <code>sys.getsizeof</code> 和 <code>tracemalloc</code> 進行精確測量：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="kn">import</span> <span class="nn">sys</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="kn">import</span> <span class="nn">tracemalloc</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="kn">from</span> <span class="nn">pympler</span> <span class="kn">import</span> <span class="n">asizeof</span>  <span class="c1"># pip install pympler</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">
</span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="k">def</span> <span class="nf">measure_object_size</span><span class="p">(</span><span class="n">obj</span><span class="p">,</span> <span class="n">label</span><span class="o">=</span><span class="s2">&#34;Object&#34;</span><span class="p">):</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">    <span class="s2">&#34;&#34;&#34;Measure object size using different methods&#34;&#34;&#34;</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">    <span class="c1"># Basic size (doesn&#39;t include referenced objects)</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">    <span class="n">basic</span> <span class="o">=</span> <span class="n">sys</span><span class="o">.</span><span class="n">getsizeof</span><span class="p">(</span><span class="n">obj</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">
</span></span><span class="line"><span class="ln">11</span><span class="cl">    <span class="c1"># Deep size (includes all referenced objects)</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">    <span class="c1"># Using pympler for accurate measurement</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">    <span class="n">deep</span> <span class="o">=</span> <span class="n">asizeof</span><span class="o">.</span><span class="n">asizeof</span><span class="p">(</span><span class="n">obj</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">
</span></span><span class="line"><span class="ln">15</span><span class="cl">    <span class="nb">print</span><span class="p">(</span><span class="sa">f</span><span class="s2">&#34;</span><span class="si">{</span><span class="n">label</span><span class="si">}</span><span class="s2">:&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">    <span class="nb">print</span><span class="p">(</span><span class="sa">f</span><span class="s2">&#34;  sys.getsizeof: </span><span class="si">{</span><span class="n">basic</span><span class="si">:</span><span class="s2">,</span><span class="si">}</span><span class="s2"> bytes&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl">    <span class="nb">print</span><span class="p">(</span><span class="sa">f</span><span class="s2">&#34;  pympler deep:  </span><span class="si">{</span><span class="n">deep</span><span class="si">:</span><span class="s2">,</span><span class="si">}</span><span class="s2"> bytes&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl">
</span></span><span class="line"><span class="ln">19</span><span class="cl">    <span class="k">return</span> <span class="n">basic</span><span class="p">,</span> <span class="n">deep</span>
</span></span><span class="line"><span class="ln">20</span><span class="cl">
</span></span><span class="line"><span class="ln">21</span><span class="cl"><span class="c1"># Compare different object types</span>
</span></span><span class="line"><span class="ln">22</span><span class="cl"><span class="n">item_without</span> <span class="o">=</span> <span class="n">ConfigItemWithoutSlots</span><span class="p">(</span><span class="s2">&#34;key&#34;</span><span class="p">,</span> <span class="s2">&#34;value&#34;</span><span class="p">,</span> <span class="p">{</span><span class="s2">&#34;a&#34;</span><span class="p">:</span> <span class="mi">1</span><span class="p">,</span> <span class="s2">&#34;b&#34;</span><span class="p">:</span> <span class="mi">2</span><span class="p">})</span>
</span></span><span class="line"><span class="ln">23</span><span class="cl"><span class="n">item_with</span> <span class="o">=</span> <span class="n">ConfigItemWithSlots</span><span class="p">(</span><span class="s2">&#34;key&#34;</span><span class="p">,</span> <span class="s2">&#34;value&#34;</span><span class="p">,</span> <span class="p">{</span><span class="s2">&#34;a&#34;</span><span class="p">:</span> <span class="mi">1</span><span class="p">,</span> <span class="s2">&#34;b&#34;</span><span class="p">:</span> <span class="mi">2</span><span class="p">})</span>
</span></span><span class="line"><span class="ln">24</span><span class="cl">
</span></span><span class="line"><span class="ln">25</span><span class="cl"><span class="n">measure_object_size</span><span class="p">(</span><span class="n">item_without</span><span class="p">,</span> <span class="s2">&#34;Without __slots__&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">26</span><span class="cl"><span class="n">measure_object_size</span><span class="p">(</span><span class="n">item_with</span><span class="p">,</span> <span class="s2">&#34;With __slots__&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">27</span><span class="cl">
</span></span><span class="line"><span class="ln">28</span><span class="cl"><span class="c1"># Using tracemalloc for allocation tracking</span>
</span></span><span class="line"><span class="ln">29</span><span class="cl"><span class="k">def</span> <span class="nf">track_allocations</span><span class="p">():</span>
</span></span><span class="line"><span class="ln">30</span><span class="cl">    <span class="s2">&#34;&#34;&#34;Track memory allocations during execution&#34;&#34;&#34;</span>
</span></span><span class="line"><span class="ln">31</span><span class="cl">    <span class="n">tracemalloc</span><span class="o">.</span><span class="n">start</span><span class="p">()</span>
</span></span><span class="line"><span class="ln">32</span><span class="cl">
</span></span><span class="line"><span class="ln">33</span><span class="cl">    <span class="c1"># Simulate creating many cached objects</span>
</span></span><span class="line"><span class="ln">34</span><span class="cl">    <span class="n">items</span> <span class="o">=</span> <span class="p">[]</span>
</span></span><span class="line"><span class="ln">35</span><span class="cl">    <span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="mi">1000</span><span class="p">):</span>
</span></span><span class="line"><span class="ln">36</span><span class="cl">        <span class="n">items</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="n">ConfigItemWithSlots</span><span class="p">(</span>
</span></span><span class="line"><span class="ln">37</span><span class="cl">            <span class="sa">f</span><span class="s2">&#34;config.item.</span><span class="si">{</span><span class="n">i</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">38</span><span class="cl">            <span class="sa">f</span><span class="s2">&#34;value_</span><span class="si">{</span><span class="n">i</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">39</span><span class="cl">            <span class="p">{</span><span class="s2">&#34;index&#34;</span><span class="p">:</span> <span class="n">i</span><span class="p">,</span> <span class="s2">&#34;active&#34;</span><span class="p">:</span> <span class="kc">True</span><span class="p">}</span>
</span></span><span class="line"><span class="ln">40</span><span class="cl">        <span class="p">))</span>
</span></span><span class="line"><span class="ln">41</span><span class="cl">
</span></span><span class="line"><span class="ln">42</span><span class="cl">    <span class="c1"># Get snapshot</span>
</span></span><span class="line"><span class="ln">43</span><span class="cl">    <span class="n">snapshot</span> <span class="o">=</span> <span class="n">tracemalloc</span><span class="o">.</span><span class="n">take_snapshot</span><span class="p">()</span>
</span></span><span class="line"><span class="ln">44</span><span class="cl">    <span class="n">top_stats</span> <span class="o">=</span> <span class="n">snapshot</span><span class="o">.</span><span class="n">statistics</span><span class="p">(</span><span class="s1">&#39;lineno&#39;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">45</span><span class="cl">
</span></span><span class="line"><span class="ln">46</span><span class="cl">    <span class="nb">print</span><span class="p">(</span><span class="s2">&#34;</span><span class="se">\n</span><span class="s2">Top 5 memory allocations:&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">47</span><span class="cl">    <span class="k">for</span> <span class="n">stat</span> <span class="ow">in</span> <span class="n">top_stats</span><span class="p">[:</span><span class="mi">5</span><span class="p">]:</span>
</span></span><span class="line"><span class="ln">48</span><span class="cl">        <span class="nb">print</span><span class="p">(</span><span class="sa">f</span><span class="s2">&#34;  </span><span class="si">{</span><span class="n">stat</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">49</span><span class="cl">
</span></span><span class="line"><span class="ln">50</span><span class="cl">    <span class="c1"># Get traced memory</span>
</span></span><span class="line"><span class="ln">51</span><span class="cl">    <span class="n">current</span><span class="p">,</span> <span class="n">peak</span> <span class="o">=</span> <span class="n">tracemalloc</span><span class="o">.</span><span class="n">get_traced_memory</span><span class="p">()</span>
</span></span><span class="line"><span class="ln">52</span><span class="cl">    <span class="nb">print</span><span class="p">(</span><span class="sa">f</span><span class="s2">&#34;</span><span class="se">\n</span><span class="s2">Current memory: </span><span class="si">{</span><span class="n">current</span> <span class="o">/</span> <span class="mi">1024</span><span class="si">:</span><span class="s2">.1f</span><span class="si">}</span><span class="s2"> KB&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">53</span><span class="cl">    <span class="nb">print</span><span class="p">(</span><span class="sa">f</span><span class="s2">&#34;Peak memory:    </span><span class="si">{</span><span class="n">peak</span> <span class="o">/</span> <span class="mi">1024</span><span class="si">:</span><span class="s2">.1f</span><span class="si">}</span><span class="s2"> KB&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">54</span><span class="cl">
</span></span><span class="line"><span class="ln">55</span><span class="cl">    <span class="n">tracemalloc</span><span class="o">.</span><span class="n">stop</span><span class="p">()</span>
</span></span><span class="line"><span class="ln">56</span><span class="cl">    <span class="k">return</span> <span class="n">items</span>
</span></span><span class="line"><span class="ln">57</span><span class="cl">
</span></span><span class="line"><span class="ln">58</span><span class="cl"><span class="n">track_allocations</span><span class="p">()</span></span></span></code></pre></div><h5 id="比較記憶體差異的完整腳本">比較記憶體差異的完整腳本</h5>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="kn">import</span> <span class="nn">sys</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="kn">import</span> <span class="nn">gc</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="kn">import</span> <span class="nn">tracemalloc</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="kn">from</span> <span class="nn">dataclasses</span> <span class="kn">import</span> <span class="n">dataclass</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="k">class</span> <span class="nc">StandardConfig</span><span class="p">:</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">    <span class="s2">&#34;&#34;&#34;Standard class with __dict__&#34;&#34;&#34;</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">    <span class="k">def</span> <span class="fm">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">key</span><span class="p">,</span> <span class="n">value</span><span class="p">,</span> <span class="n">metadata</span><span class="p">):</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">key</span> <span class="o">=</span> <span class="n">key</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">value</span> <span class="o">=</span> <span class="n">value</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">metadata</span> <span class="o">=</span> <span class="n">metadata</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">hits</span> <span class="o">=</span> <span class="mi">0</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">
</span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="k">class</span> <span class="nc">SlottedConfig</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">    <span class="s2">&#34;&#34;&#34;Optimized with __slots__&#34;&#34;&#34;</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">    <span class="vm">__slots__</span> <span class="o">=</span> <span class="p">[</span><span class="s1">&#39;key&#39;</span><span class="p">,</span> <span class="s1">&#39;value&#39;</span><span class="p">,</span> <span class="s1">&#39;metadata&#39;</span><span class="p">,</span> <span class="s1">&#39;hits&#39;</span><span class="p">]</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl">
</span></span><span class="line"><span class="ln">18</span><span class="cl">    <span class="k">def</span> <span class="fm">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">key</span><span class="p">,</span> <span class="n">value</span><span class="p">,</span> <span class="n">metadata</span><span class="p">):</span>
</span></span><span class="line"><span class="ln">19</span><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">key</span> <span class="o">=</span> <span class="n">key</span>
</span></span><span class="line"><span class="ln">20</span><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">value</span> <span class="o">=</span> <span class="n">value</span>
</span></span><span class="line"><span class="ln">21</span><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">metadata</span> <span class="o">=</span> <span class="n">metadata</span>
</span></span><span class="line"><span class="ln">22</span><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">hits</span> <span class="o">=</span> <span class="mi">0</span>
</span></span><span class="line"><span class="ln">23</span><span class="cl">
</span></span><span class="line"><span class="ln">24</span><span class="cl"><span class="nd">@dataclass</span>
</span></span><span class="line"><span class="ln">25</span><span class="cl"><span class="k">class</span> <span class="nc">DataclassConfig</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">26</span><span class="cl">    <span class="s2">&#34;&#34;&#34;Using dataclass&#34;&#34;&#34;</span>
</span></span><span class="line"><span class="ln">27</span><span class="cl">    <span class="n">key</span><span class="p">:</span> <span class="nb">str</span>
</span></span><span class="line"><span class="ln">28</span><span class="cl">    <span class="n">value</span><span class="p">:</span> <span class="nb">str</span>
</span></span><span class="line"><span class="ln">29</span><span class="cl">    <span class="n">metadata</span><span class="p">:</span> <span class="nb">dict</span>
</span></span><span class="line"><span class="ln">30</span><span class="cl">    <span class="n">hits</span><span class="p">:</span> <span class="nb">int</span> <span class="o">=</span> <span class="mi">0</span>
</span></span><span class="line"><span class="ln">31</span><span class="cl">
</span></span><span class="line"><span class="ln">32</span><span class="cl"><span class="nd">@dataclass</span><span class="p">(</span><span class="n">slots</span><span class="o">=</span><span class="kc">True</span><span class="p">)</span>  <span class="c1"># Python 3.10+</span>
</span></span><span class="line"><span class="ln">33</span><span class="cl"><span class="k">class</span> <span class="nc">SlottedDataclass</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">34</span><span class="cl">    <span class="s2">&#34;&#34;&#34;Dataclass with __slots__&#34;&#34;&#34;</span>
</span></span><span class="line"><span class="ln">35</span><span class="cl">    <span class="n">key</span><span class="p">:</span> <span class="nb">str</span>
</span></span><span class="line"><span class="ln">36</span><span class="cl">    <span class="n">value</span><span class="p">:</span> <span class="nb">str</span>
</span></span><span class="line"><span class="ln">37</span><span class="cl">    <span class="n">metadata</span><span class="p">:</span> <span class="nb">dict</span>
</span></span><span class="line"><span class="ln">38</span><span class="cl">    <span class="n">hits</span><span class="p">:</span> <span class="nb">int</span> <span class="o">=</span> <span class="mi">0</span>
</span></span><span class="line"><span class="ln">39</span><span class="cl">
</span></span><span class="line"><span class="ln">40</span><span class="cl"><span class="k">def</span> <span class="nf">benchmark_memory</span><span class="p">(</span><span class="bp">cls</span><span class="p">,</span> <span class="n">count</span><span class="o">=</span><span class="mi">10000</span><span class="p">,</span> <span class="n">label</span><span class="o">=</span><span class="s2">&#34;&#34;</span><span class="p">):</span>
</span></span><span class="line"><span class="ln">41</span><span class="cl">    <span class="s2">&#34;&#34;&#34;Benchmark memory usage for a class&#34;&#34;&#34;</span>
</span></span><span class="line"><span class="ln">42</span><span class="cl">    <span class="n">gc</span><span class="o">.</span><span class="n">collect</span><span class="p">()</span>
</span></span><span class="line"><span class="ln">43</span><span class="cl">    <span class="n">tracemalloc</span><span class="o">.</span><span class="n">start</span><span class="p">()</span>
</span></span><span class="line"><span class="ln">44</span><span class="cl">
</span></span><span class="line"><span class="ln">45</span><span class="cl">    <span class="n">objects</span> <span class="o">=</span> <span class="p">[</span>
</span></span><span class="line"><span class="ln">46</span><span class="cl">        <span class="bp">cls</span><span class="p">(</span><span class="sa">f</span><span class="s2">&#34;key_</span><span class="si">{</span><span class="n">i</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">,</span> <span class="sa">f</span><span class="s2">&#34;value_</span><span class="si">{</span><span class="n">i</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">,</span> <span class="p">{</span><span class="s2">&#34;index&#34;</span><span class="p">:</span> <span class="n">i</span><span class="p">})</span>
</span></span><span class="line"><span class="ln">47</span><span class="cl">        <span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="n">count</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">48</span><span class="cl">    <span class="p">]</span>
</span></span><span class="line"><span class="ln">49</span><span class="cl">
</span></span><span class="line"><span class="ln">50</span><span class="cl">    <span class="n">current</span><span class="p">,</span> <span class="n">peak</span> <span class="o">=</span> <span class="n">tracemalloc</span><span class="o">.</span><span class="n">get_traced_memory</span><span class="p">()</span>
</span></span><span class="line"><span class="ln">51</span><span class="cl">    <span class="n">tracemalloc</span><span class="o">.</span><span class="n">stop</span><span class="p">()</span>
</span></span><span class="line"><span class="ln">52</span><span class="cl">
</span></span><span class="line"><span class="ln">53</span><span class="cl">    <span class="n">per_object</span> <span class="o">=</span> <span class="n">current</span> <span class="o">/</span> <span class="n">count</span>
</span></span><span class="line"><span class="ln">54</span><span class="cl">    <span class="nb">print</span><span class="p">(</span><span class="sa">f</span><span class="s2">&#34;</span><span class="si">{</span><span class="n">label</span> <span class="ow">or</span> <span class="bp">cls</span><span class="o">.</span><span class="vm">__name__</span><span class="si">:</span><span class="s2">25</span><span class="si">}</span><span class="s2"> | &#34;</span>
</span></span><span class="line"><span class="ln">55</span><span class="cl">          <span class="sa">f</span><span class="s2">&#34;</span><span class="si">{</span><span class="n">current</span><span class="o">/</span><span class="mi">1024</span><span class="si">:</span><span class="s2">8.1f</span><span class="si">}</span><span class="s2"> KB | &#34;</span>
</span></span><span class="line"><span class="ln">56</span><span class="cl">          <span class="sa">f</span><span class="s2">&#34;</span><span class="si">{</span><span class="n">per_object</span><span class="si">:</span><span class="s2">6.1f</span><span class="si">}</span><span class="s2"> B/obj&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">57</span><span class="cl">
</span></span><span class="line"><span class="ln">58</span><span class="cl">    <span class="k">return</span> <span class="n">objects</span>  <span class="c1"># Keep reference to prevent GC</span>
</span></span><span class="line"><span class="ln">59</span><span class="cl">
</span></span><span class="line"><span class="ln">60</span><span class="cl"><span class="nb">print</span><span class="p">(</span><span class="sa">f</span><span class="s2">&#34;</span><span class="si">{</span><span class="s1">&#39;Class&#39;</span><span class="si">:</span><span class="s2">25</span><span class="si">}</span><span class="s2"> | </span><span class="si">{</span><span class="s1">&#39;Total&#39;</span><span class="si">:</span><span class="s2">&gt;10</span><span class="si">}</span><span class="s2"> | </span><span class="si">{</span><span class="s1">&#39;Per Object&#39;</span><span class="si">:</span><span class="s2">&gt;10</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">61</span><span class="cl"><span class="nb">print</span><span class="p">(</span><span class="s2">&#34;-&#34;</span> <span class="o">*</span> <span class="mi">55</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">62</span><span class="cl">
</span></span><span class="line"><span class="ln">63</span><span class="cl"><span class="n">benchmark_memory</span><span class="p">(</span><span class="n">StandardConfig</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">64</span><span class="cl"><span class="n">benchmark_memory</span><span class="p">(</span><span class="n">SlottedConfig</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">65</span><span class="cl"><span class="n">benchmark_memory</span><span class="p">(</span><span class="n">DataclassConfig</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">66</span><span class="cl"><span class="n">benchmark_memory</span><span class="p">(</span><span class="n">SlottedDataclass</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">67</span><span class="cl">
</span></span><span class="line"><span class="ln">68</span><span class="cl"><span class="c1"># Typical output:</span>
</span></span><span class="line"><span class="ln">69</span><span class="cl"><span class="c1"># Class                     |      Total |  Per Object</span>
</span></span><span class="line"><span class="ln">70</span><span class="cl"><span class="c1"># -------------------------------------------------------</span>
</span></span><span class="line"><span class="ln">71</span><span class="cl"><span class="c1"># StandardConfig            |   2578.5 KB |  263.6 B/obj</span>
</span></span><span class="line"><span class="ln">72</span><span class="cl"><span class="c1"># SlottedConfig             |    859.4 KB |   87.9 B/obj</span>
</span></span><span class="line"><span class="ln">73</span><span class="cl"><span class="c1"># DataclassConfig           |   2656.3 KB |  271.6 B/obj</span>
</span></span><span class="line"><span class="ln">74</span><span class="cl"><span class="c1"># SlottedDataclass          |    898.4 KB |   91.9 B/obj</span></span></span></code></pre></div><hr>
<h3 id="完整程式碼">完整程式碼</h3>
<p>以下是整合所有優化技術的完整實作：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln">  1</span><span class="cl"><span class="s2">&#34;&#34;&#34;
</span></span></span><span class="line"><span class="ln">  2</span><span class="cl"><span class="s2">Memory-optimized configuration cache system.
</span></span></span><span class="line"><span class="ln">  3</span><span class="cl"><span class="s2">
</span></span></span><span class="line"><span class="ln">  4</span><span class="cl"><span class="s2">This module demonstrates:
</span></span></span><span class="line"><span class="ln">  5</span><span class="cl"><span class="s2">- Using __slots__ to reduce object memory footprint
</span></span></span><span class="line"><span class="ln">  6</span><span class="cl"><span class="s2">- Using weakref for automatic cache cleanup
</span></span></span><span class="line"><span class="ln">  7</span><span class="cl"><span class="s2">- Using tracemalloc for memory profiling
</span></span></span><span class="line"><span class="ln">  8</span><span class="cl"><span class="s2">
</span></span></span><span class="line"><span class="ln">  9</span><span class="cl"><span class="s2">Based on patterns from .claude/lib/config_loader.py
</span></span></span><span class="line"><span class="ln"> 10</span><span class="cl"><span class="s2">&#34;&#34;&#34;</span>
</span></span><span class="line"><span class="ln"> 11</span><span class="cl">
</span></span><span class="line"><span class="ln"> 12</span><span class="cl"><span class="kn">import</span> <span class="nn">weakref</span>
</span></span><span class="line"><span class="ln"> 13</span><span class="cl"><span class="kn">import</span> <span class="nn">sys</span>
</span></span><span class="line"><span class="ln"> 14</span><span class="cl"><span class="kn">import</span> <span class="nn">gc</span>
</span></span><span class="line"><span class="ln"> 15</span><span class="cl"><span class="kn">import</span> <span class="nn">tracemalloc</span>
</span></span><span class="line"><span class="ln"> 16</span><span class="cl"><span class="kn">from</span> <span class="nn">typing</span> <span class="kn">import</span> <span class="n">Any</span><span class="p">,</span> <span class="n">Callable</span><span class="p">,</span> <span class="n">Generic</span><span class="p">,</span> <span class="n">Optional</span><span class="p">,</span> <span class="n">TypeVar</span>
</span></span><span class="line"><span class="ln"> 17</span><span class="cl"><span class="kn">from</span> <span class="nn">pathlib</span> <span class="kn">import</span> <span class="n">Path</span>
</span></span><span class="line"><span class="ln"> 18</span><span class="cl"><span class="kn">from</span> <span class="nn">datetime</span> <span class="kn">import</span> <span class="n">datetime</span>
</span></span><span class="line"><span class="ln"> 19</span><span class="cl">
</span></span><span class="line"><span class="ln"> 20</span><span class="cl"><span class="n">T</span> <span class="o">=</span> <span class="n">TypeVar</span><span class="p">(</span><span class="s1">&#39;T&#39;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 21</span><span class="cl">
</span></span><span class="line"><span class="ln"> 22</span><span class="cl"><span class="k">class</span> <span class="nc">ConfigEntry</span><span class="p">:</span>
</span></span><span class="line"><span class="ln"> 23</span><span class="cl">    <span class="s2">&#34;&#34;&#34;
</span></span></span><span class="line"><span class="ln"> 24</span><span class="cl"><span class="s2">    Memory-optimized configuration entry.
</span></span></span><span class="line"><span class="ln"> 25</span><span class="cl"><span class="s2">
</span></span></span><span class="line"><span class="ln"> 26</span><span class="cl"><span class="s2">    Uses __slots__ to reduce memory footprint by ~3x compared
</span></span></span><span class="line"><span class="ln"> 27</span><span class="cl"><span class="s2">    to regular classes.
</span></span></span><span class="line"><span class="ln"> 28</span><span class="cl"><span class="s2">    &#34;&#34;&#34;</span>
</span></span><span class="line"><span class="ln"> 29</span><span class="cl">    <span class="vm">__slots__</span> <span class="o">=</span> <span class="p">[</span>
</span></span><span class="line"><span class="ln"> 30</span><span class="cl">        <span class="s1">&#39;key&#39;</span><span class="p">,</span> <span class="s1">&#39;value&#39;</span><span class="p">,</span> <span class="s1">&#39;source&#39;</span><span class="p">,</span> <span class="s1">&#39;loaded_at&#39;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 31</span><span class="cl">        <span class="s1">&#39;access_count&#39;</span><span class="p">,</span> <span class="s1">&#39;__weakref__&#39;</span>
</span></span><span class="line"><span class="ln"> 32</span><span class="cl">    <span class="p">]</span>
</span></span><span class="line"><span class="ln"> 33</span><span class="cl">
</span></span><span class="line"><span class="ln"> 34</span><span class="cl">    <span class="k">def</span> <span class="fm">__init__</span><span class="p">(</span>
</span></span><span class="line"><span class="ln"> 35</span><span class="cl">        <span class="bp">self</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 36</span><span class="cl">        <span class="n">key</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 37</span><span class="cl">        <span class="n">value</span><span class="p">:</span> <span class="n">Any</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 38</span><span class="cl">        <span class="n">source</span><span class="p">:</span> <span class="n">Optional</span><span class="p">[</span><span class="nb">str</span><span class="p">]</span> <span class="o">=</span> <span class="kc">None</span>
</span></span><span class="line"><span class="ln"> 39</span><span class="cl">    <span class="p">):</span>
</span></span><span class="line"><span class="ln"> 40</span><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">key</span> <span class="o">=</span> <span class="n">key</span>
</span></span><span class="line"><span class="ln"> 41</span><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">value</span> <span class="o">=</span> <span class="n">value</span>
</span></span><span class="line"><span class="ln"> 42</span><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">source</span> <span class="o">=</span> <span class="n">source</span>
</span></span><span class="line"><span class="ln"> 43</span><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">loaded_at</span> <span class="o">=</span> <span class="n">datetime</span><span class="o">.</span><span class="n">now</span><span class="p">()</span>
</span></span><span class="line"><span class="ln"> 44</span><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">access_count</span> <span class="o">=</span> <span class="mi">0</span>
</span></span><span class="line"><span class="ln"> 45</span><span class="cl">
</span></span><span class="line"><span class="ln"> 46</span><span class="cl">    <span class="k">def</span> <span class="fm">__repr__</span><span class="p">(</span><span class="bp">self</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">str</span><span class="p">:</span>
</span></span><span class="line"><span class="ln"> 47</span><span class="cl">        <span class="k">return</span> <span class="p">(</span>
</span></span><span class="line"><span class="ln"> 48</span><span class="cl">            <span class="sa">f</span><span class="s2">&#34;ConfigEntry(key=</span><span class="si">{</span><span class="bp">self</span><span class="o">.</span><span class="n">key</span><span class="si">!r}</span><span class="s2">, &#34;</span>
</span></span><span class="line"><span class="ln"> 49</span><span class="cl">            <span class="sa">f</span><span class="s2">&#34;value=</span><span class="si">{</span><span class="bp">self</span><span class="o">.</span><span class="n">value</span><span class="si">!r}</span><span class="s2">, &#34;</span>
</span></span><span class="line"><span class="ln"> 50</span><span class="cl">            <span class="sa">f</span><span class="s2">&#34;accesses=</span><span class="si">{</span><span class="bp">self</span><span class="o">.</span><span class="n">access_count</span><span class="si">}</span><span class="s2">)&#34;</span>
</span></span><span class="line"><span class="ln"> 51</span><span class="cl">        <span class="p">)</span>
</span></span><span class="line"><span class="ln"> 52</span><span class="cl">
</span></span><span class="line"><span class="ln"> 53</span><span class="cl">    <span class="k">def</span> <span class="nf">touch</span><span class="p">(</span><span class="bp">self</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kc">None</span><span class="p">:</span>
</span></span><span class="line"><span class="ln"> 54</span><span class="cl">        <span class="s2">&#34;&#34;&#34;Record an access to this entry&#34;&#34;&#34;</span>
</span></span><span class="line"><span class="ln"> 55</span><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">access_count</span> <span class="o">+=</span> <span class="mi">1</span>
</span></span><span class="line"><span class="ln"> 56</span><span class="cl">
</span></span><span class="line"><span class="ln"> 57</span><span class="cl"><span class="k">class</span> <span class="nc">SmartConfigCache</span><span class="p">:</span>
</span></span><span class="line"><span class="ln"> 58</span><span class="cl">    <span class="s2">&#34;&#34;&#34;
</span></span></span><span class="line"><span class="ln"> 59</span><span class="cl"><span class="s2">    Smart configuration cache with automatic memory management.
</span></span></span><span class="line"><span class="ln"> 60</span><span class="cl"><span class="s2">
</span></span></span><span class="line"><span class="ln"> 61</span><span class="cl"><span class="s2">    Features:
</span></span></span><span class="line"><span class="ln"> 62</span><span class="cl"><span class="s2">    - Weak references for automatic cleanup
</span></span></span><span class="line"><span class="ln"> 63</span><span class="cl"><span class="s2">    - Memory usage tracking
</span></span></span><span class="line"><span class="ln"> 64</span><span class="cl"><span class="s2">    - Hit/miss statistics
</span></span></span><span class="line"><span class="ln"> 65</span><span class="cl"><span class="s2">    - Optional size limits
</span></span></span><span class="line"><span class="ln"> 66</span><span class="cl"><span class="s2">
</span></span></span><span class="line"><span class="ln"> 67</span><span class="cl"><span class="s2">    Example:
</span></span></span><span class="line"><span class="ln"> 68</span><span class="cl"><span class="s2">        cache = SmartConfigCache(max_size=1000)
</span></span></span><span class="line"><span class="ln"> 69</span><span class="cl"><span class="s2">
</span></span></span><span class="line"><span class="ln"> 70</span><span class="cl"><span class="s2">        # Get or create config
</span></span></span><span class="line"><span class="ln"> 71</span><span class="cl"><span class="s2">        config = cache.get_or_create(
</span></span></span><span class="line"><span class="ln"> 72</span><span class="cl"><span class="s2">            &#34;database.host&#34;,
</span></span></span><span class="line"><span class="ln"> 73</span><span class="cl"><span class="s2">            lambda: ConfigEntry(&#34;database.host&#34;, &#34;localhost&#34;, &#34;env&#34;)
</span></span></span><span class="line"><span class="ln"> 74</span><span class="cl"><span class="s2">        )
</span></span></span><span class="line"><span class="ln"> 75</span><span class="cl"><span class="s2">
</span></span></span><span class="line"><span class="ln"> 76</span><span class="cl"><span class="s2">        # Check stats
</span></span></span><span class="line"><span class="ln"> 77</span><span class="cl"><span class="s2">        print(cache.stats())
</span></span></span><span class="line"><span class="ln"> 78</span><span class="cl"><span class="s2">    &#34;&#34;&#34;</span>
</span></span><span class="line"><span class="ln"> 79</span><span class="cl">
</span></span><span class="line"><span class="ln"> 80</span><span class="cl">    <span class="k">def</span> <span class="fm">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">max_size</span><span class="p">:</span> <span class="n">Optional</span><span class="p">[</span><span class="nb">int</span><span class="p">]</span> <span class="o">=</span> <span class="kc">None</span><span class="p">):</span>
</span></span><span class="line"><span class="ln"> 81</span><span class="cl">        <span class="s2">&#34;&#34;&#34;
</span></span></span><span class="line"><span class="ln"> 82</span><span class="cl"><span class="s2">        Initialize the cache.
</span></span></span><span class="line"><span class="ln"> 83</span><span class="cl"><span class="s2">
</span></span></span><span class="line"><span class="ln"> 84</span><span class="cl"><span class="s2">        Args:
</span></span></span><span class="line"><span class="ln"> 85</span><span class="cl"><span class="s2">            max_size: Maximum number of entries. None for unlimited.
</span></span></span><span class="line"><span class="ln"> 86</span><span class="cl"><span class="s2">        &#34;&#34;&#34;</span>
</span></span><span class="line"><span class="ln"> 87</span><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">_cache</span><span class="p">:</span> <span class="n">weakref</span><span class="o">.</span><span class="n">WeakValueDictionary</span><span class="p">[</span><span class="nb">str</span><span class="p">,</span> <span class="n">ConfigEntry</span><span class="p">]</span> <span class="o">=</span> <span class="p">(</span>
</span></span><span class="line"><span class="ln"> 88</span><span class="cl">            <span class="n">weakref</span><span class="o">.</span><span class="n">WeakValueDictionary</span><span class="p">()</span>
</span></span><span class="line"><span class="ln"> 89</span><span class="cl">        <span class="p">)</span>
</span></span><span class="line"><span class="ln"> 90</span><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">_strong_refs</span><span class="p">:</span> <span class="nb">dict</span><span class="p">[</span><span class="nb">str</span><span class="p">,</span> <span class="n">ConfigEntry</span><span class="p">]</span> <span class="o">=</span> <span class="p">{}</span>  <span class="c1"># Keep important items</span>
</span></span><span class="line"><span class="ln"> 91</span><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">_max_size</span> <span class="o">=</span> <span class="n">max_size</span>
</span></span><span class="line"><span class="ln"> 92</span><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">_hits</span> <span class="o">=</span> <span class="mi">0</span>
</span></span><span class="line"><span class="ln"> 93</span><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">_misses</span> <span class="o">=</span> <span class="mi">0</span>
</span></span><span class="line"><span class="ln"> 94</span><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">_evictions</span> <span class="o">=</span> <span class="mi">0</span>
</span></span><span class="line"><span class="ln"> 95</span><span class="cl">
</span></span><span class="line"><span class="ln"> 96</span><span class="cl">    <span class="k">def</span> <span class="nf">get</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">key</span><span class="p">:</span> <span class="nb">str</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="n">Optional</span><span class="p">[</span><span class="n">ConfigEntry</span><span class="p">]:</span>
</span></span><span class="line"><span class="ln"> 97</span><span class="cl">        <span class="s2">&#34;&#34;&#34;
</span></span></span><span class="line"><span class="ln"> 98</span><span class="cl"><span class="s2">        Get entry from cache.
</span></span></span><span class="line"><span class="ln"> 99</span><span class="cl"><span class="s2">
</span></span></span><span class="line"><span class="ln">100</span><span class="cl"><span class="s2">        Args:
</span></span></span><span class="line"><span class="ln">101</span><span class="cl"><span class="s2">            key: Configuration key
</span></span></span><span class="line"><span class="ln">102</span><span class="cl"><span class="s2">
</span></span></span><span class="line"><span class="ln">103</span><span class="cl"><span class="s2">        Returns:
</span></span></span><span class="line"><span class="ln">104</span><span class="cl"><span class="s2">            ConfigEntry if found, None otherwise
</span></span></span><span class="line"><span class="ln">105</span><span class="cl"><span class="s2">        &#34;&#34;&#34;</span>
</span></span><span class="line"><span class="ln">106</span><span class="cl">        <span class="c1"># Check strong refs first</span>
</span></span><span class="line"><span class="ln">107</span><span class="cl">        <span class="n">entry</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_strong_refs</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="n">key</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">108</span><span class="cl">        <span class="k">if</span> <span class="n">entry</span> <span class="ow">is</span> <span class="ow">not</span> <span class="kc">None</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">109</span><span class="cl">            <span class="bp">self</span><span class="o">.</span><span class="n">_hits</span> <span class="o">+=</span> <span class="mi">1</span>
</span></span><span class="line"><span class="ln">110</span><span class="cl">            <span class="n">entry</span><span class="o">.</span><span class="n">touch</span><span class="p">()</span>
</span></span><span class="line"><span class="ln">111</span><span class="cl">            <span class="k">return</span> <span class="n">entry</span>
</span></span><span class="line"><span class="ln">112</span><span class="cl">
</span></span><span class="line"><span class="ln">113</span><span class="cl">        <span class="c1"># Then check weak refs</span>
</span></span><span class="line"><span class="ln">114</span><span class="cl">        <span class="n">entry</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_cache</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="n">key</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">115</span><span class="cl">        <span class="k">if</span> <span class="n">entry</span> <span class="ow">is</span> <span class="ow">not</span> <span class="kc">None</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">116</span><span class="cl">            <span class="bp">self</span><span class="o">.</span><span class="n">_hits</span> <span class="o">+=</span> <span class="mi">1</span>
</span></span><span class="line"><span class="ln">117</span><span class="cl">            <span class="n">entry</span><span class="o">.</span><span class="n">touch</span><span class="p">()</span>
</span></span><span class="line"><span class="ln">118</span><span class="cl">            <span class="k">return</span> <span class="n">entry</span>
</span></span><span class="line"><span class="ln">119</span><span class="cl">
</span></span><span class="line"><span class="ln">120</span><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">_misses</span> <span class="o">+=</span> <span class="mi">1</span>
</span></span><span class="line"><span class="ln">121</span><span class="cl">        <span class="k">return</span> <span class="kc">None</span>
</span></span><span class="line"><span class="ln">122</span><span class="cl">
</span></span><span class="line"><span class="ln">123</span><span class="cl">    <span class="k">def</span> <span class="nf">get_or_create</span><span class="p">(</span>
</span></span><span class="line"><span class="ln">124</span><span class="cl">        <span class="bp">self</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">125</span><span class="cl">        <span class="n">key</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">126</span><span class="cl">        <span class="n">factory</span><span class="p">:</span> <span class="n">Callable</span><span class="p">[[],</span> <span class="n">ConfigEntry</span><span class="p">],</span>
</span></span><span class="line"><span class="ln">127</span><span class="cl">        <span class="n">keep_strong</span><span class="p">:</span> <span class="nb">bool</span> <span class="o">=</span> <span class="kc">False</span>
</span></span><span class="line"><span class="ln">128</span><span class="cl">    <span class="p">)</span> <span class="o">-&gt;</span> <span class="n">ConfigEntry</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">129</span><span class="cl">        <span class="s2">&#34;&#34;&#34;
</span></span></span><span class="line"><span class="ln">130</span><span class="cl"><span class="s2">        Get existing entry or create new one.
</span></span></span><span class="line"><span class="ln">131</span><span class="cl"><span class="s2">
</span></span></span><span class="line"><span class="ln">132</span><span class="cl"><span class="s2">        Args:
</span></span></span><span class="line"><span class="ln">133</span><span class="cl"><span class="s2">            key: Configuration key
</span></span></span><span class="line"><span class="ln">134</span><span class="cl"><span class="s2">            factory: Function to create entry if not found
</span></span></span><span class="line"><span class="ln">135</span><span class="cl"><span class="s2">            keep_strong: If True, keep a strong reference (won&#39;t auto-cleanup)
</span></span></span><span class="line"><span class="ln">136</span><span class="cl"><span class="s2">
</span></span></span><span class="line"><span class="ln">137</span><span class="cl"><span class="s2">        Returns:
</span></span></span><span class="line"><span class="ln">138</span><span class="cl"><span class="s2">            Existing or newly created ConfigEntry
</span></span></span><span class="line"><span class="ln">139</span><span class="cl"><span class="s2">        &#34;&#34;&#34;</span>
</span></span><span class="line"><span class="ln">140</span><span class="cl">        <span class="n">entry</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="n">key</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">141</span><span class="cl">        <span class="k">if</span> <span class="n">entry</span> <span class="ow">is</span> <span class="ow">not</span> <span class="kc">None</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">142</span><span class="cl">            <span class="k">return</span> <span class="n">entry</span>
</span></span><span class="line"><span class="ln">143</span><span class="cl">
</span></span><span class="line"><span class="ln">144</span><span class="cl">        <span class="c1"># Create new entry</span>
</span></span><span class="line"><span class="ln">145</span><span class="cl">        <span class="n">entry</span> <span class="o">=</span> <span class="n">factory</span><span class="p">()</span>
</span></span><span class="line"><span class="ln">146</span><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">_cache</span><span class="p">[</span><span class="n">key</span><span class="p">]</span> <span class="o">=</span> <span class="n">entry</span>
</span></span><span class="line"><span class="ln">147</span><span class="cl">
</span></span><span class="line"><span class="ln">148</span><span class="cl">        <span class="k">if</span> <span class="n">keep_strong</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">149</span><span class="cl">            <span class="bp">self</span><span class="o">.</span><span class="n">_enforce_size_limit</span><span class="p">()</span>
</span></span><span class="line"><span class="ln">150</span><span class="cl">            <span class="bp">self</span><span class="o">.</span><span class="n">_strong_refs</span><span class="p">[</span><span class="n">key</span><span class="p">]</span> <span class="o">=</span> <span class="n">entry</span>
</span></span><span class="line"><span class="ln">151</span><span class="cl">
</span></span><span class="line"><span class="ln">152</span><span class="cl">        <span class="k">return</span> <span class="n">entry</span>
</span></span><span class="line"><span class="ln">153</span><span class="cl">
</span></span><span class="line"><span class="ln">154</span><span class="cl">    <span class="k">def</span> <span class="nf">_enforce_size_limit</span><span class="p">(</span><span class="bp">self</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kc">None</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">155</span><span class="cl">        <span class="s2">&#34;&#34;&#34;Evict old entries if cache is full&#34;&#34;&#34;</span>
</span></span><span class="line"><span class="ln">156</span><span class="cl">        <span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">_max_size</span> <span class="ow">is</span> <span class="kc">None</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">157</span><span class="cl">            <span class="k">return</span>
</span></span><span class="line"><span class="ln">158</span><span class="cl">
</span></span><span class="line"><span class="ln">159</span><span class="cl">        <span class="k">while</span> <span class="nb">len</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">_strong_refs</span><span class="p">)</span> <span class="o">&gt;=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_max_size</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">160</span><span class="cl">            <span class="c1"># Evict least accessed entry</span>
</span></span><span class="line"><span class="ln">161</span><span class="cl">            <span class="k">if</span> <span class="ow">not</span> <span class="bp">self</span><span class="o">.</span><span class="n">_strong_refs</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">162</span><span class="cl">                <span class="k">break</span>
</span></span><span class="line"><span class="ln">163</span><span class="cl">
</span></span><span class="line"><span class="ln">164</span><span class="cl">            <span class="n">min_key</span> <span class="o">=</span> <span class="nb">min</span><span class="p">(</span>
</span></span><span class="line"><span class="ln">165</span><span class="cl">                <span class="bp">self</span><span class="o">.</span><span class="n">_strong_refs</span><span class="o">.</span><span class="n">keys</span><span class="p">(),</span>
</span></span><span class="line"><span class="ln">166</span><span class="cl">                <span class="n">key</span><span class="o">=</span><span class="k">lambda</span> <span class="n">k</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">_strong_refs</span><span class="p">[</span><span class="n">k</span><span class="p">]</span><span class="o">.</span><span class="n">access_count</span>
</span></span><span class="line"><span class="ln">167</span><span class="cl">            <span class="p">)</span>
</span></span><span class="line"><span class="ln">168</span><span class="cl">            <span class="k">del</span> <span class="bp">self</span><span class="o">.</span><span class="n">_strong_refs</span><span class="p">[</span><span class="n">min_key</span><span class="p">]</span>
</span></span><span class="line"><span class="ln">169</span><span class="cl">            <span class="bp">self</span><span class="o">.</span><span class="n">_evictions</span> <span class="o">+=</span> <span class="mi">1</span>
</span></span><span class="line"><span class="ln">170</span><span class="cl">
</span></span><span class="line"><span class="ln">171</span><span class="cl">    <span class="k">def</span> <span class="nf">pin</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">key</span><span class="p">:</span> <span class="nb">str</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">bool</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">172</span><span class="cl">        <span class="s2">&#34;&#34;&#34;
</span></span></span><span class="line"><span class="ln">173</span><span class="cl"><span class="s2">        Pin an entry to prevent automatic cleanup.
</span></span></span><span class="line"><span class="ln">174</span><span class="cl"><span class="s2">
</span></span></span><span class="line"><span class="ln">175</span><span class="cl"><span class="s2">        Args:
</span></span></span><span class="line"><span class="ln">176</span><span class="cl"><span class="s2">            key: Configuration key
</span></span></span><span class="line"><span class="ln">177</span><span class="cl"><span class="s2">
</span></span></span><span class="line"><span class="ln">178</span><span class="cl"><span class="s2">        Returns:
</span></span></span><span class="line"><span class="ln">179</span><span class="cl"><span class="s2">            True if entry was pinned, False if not found
</span></span></span><span class="line"><span class="ln">180</span><span class="cl"><span class="s2">        &#34;&#34;&#34;</span>
</span></span><span class="line"><span class="ln">181</span><span class="cl">        <span class="n">entry</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_cache</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="n">key</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">182</span><span class="cl">        <span class="k">if</span> <span class="n">entry</span> <span class="ow">is</span> <span class="kc">None</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">183</span><span class="cl">            <span class="k">return</span> <span class="kc">False</span>
</span></span><span class="line"><span class="ln">184</span><span class="cl">
</span></span><span class="line"><span class="ln">185</span><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">_enforce_size_limit</span><span class="p">()</span>
</span></span><span class="line"><span class="ln">186</span><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">_strong_refs</span><span class="p">[</span><span class="n">key</span><span class="p">]</span> <span class="o">=</span> <span class="n">entry</span>
</span></span><span class="line"><span class="ln">187</span><span class="cl">        <span class="k">return</span> <span class="kc">True</span>
</span></span><span class="line"><span class="ln">188</span><span class="cl">
</span></span><span class="line"><span class="ln">189</span><span class="cl">    <span class="k">def</span> <span class="nf">unpin</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">key</span><span class="p">:</span> <span class="nb">str</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">bool</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">190</span><span class="cl">        <span class="s2">&#34;&#34;&#34;
</span></span></span><span class="line"><span class="ln">191</span><span class="cl"><span class="s2">        Unpin an entry to allow automatic cleanup.
</span></span></span><span class="line"><span class="ln">192</span><span class="cl"><span class="s2">
</span></span></span><span class="line"><span class="ln">193</span><span class="cl"><span class="s2">        Args:
</span></span></span><span class="line"><span class="ln">194</span><span class="cl"><span class="s2">            key: Configuration key
</span></span></span><span class="line"><span class="ln">195</span><span class="cl"><span class="s2">
</span></span></span><span class="line"><span class="ln">196</span><span class="cl"><span class="s2">        Returns:
</span></span></span><span class="line"><span class="ln">197</span><span class="cl"><span class="s2">            True if entry was unpinned, False if not found
</span></span></span><span class="line"><span class="ln">198</span><span class="cl"><span class="s2">        &#34;&#34;&#34;</span>
</span></span><span class="line"><span class="ln">199</span><span class="cl">        <span class="k">if</span> <span class="n">key</span> <span class="ow">in</span> <span class="bp">self</span><span class="o">.</span><span class="n">_strong_refs</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">200</span><span class="cl">            <span class="k">del</span> <span class="bp">self</span><span class="o">.</span><span class="n">_strong_refs</span><span class="p">[</span><span class="n">key</span><span class="p">]</span>
</span></span><span class="line"><span class="ln">201</span><span class="cl">            <span class="k">return</span> <span class="kc">True</span>
</span></span><span class="line"><span class="ln">202</span><span class="cl">        <span class="k">return</span> <span class="kc">False</span>
</span></span><span class="line"><span class="ln">203</span><span class="cl">
</span></span><span class="line"><span class="ln">204</span><span class="cl">    <span class="k">def</span> <span class="nf">clear</span><span class="p">(</span><span class="bp">self</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kc">None</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">205</span><span class="cl">        <span class="s2">&#34;&#34;&#34;Clear all cached entries&#34;&#34;&#34;</span>
</span></span><span class="line"><span class="ln">206</span><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">_cache</span><span class="o">.</span><span class="n">clear</span><span class="p">()</span>
</span></span><span class="line"><span class="ln">207</span><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">_strong_refs</span><span class="o">.</span><span class="n">clear</span><span class="p">()</span>
</span></span><span class="line"><span class="ln">208</span><span class="cl">
</span></span><span class="line"><span class="ln">209</span><span class="cl">    <span class="k">def</span> <span class="nf">stats</span><span class="p">(</span><span class="bp">self</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">dict</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">210</span><span class="cl">        <span class="s2">&#34;&#34;&#34;
</span></span></span><span class="line"><span class="ln">211</span><span class="cl"><span class="s2">        Get cache statistics.
</span></span></span><span class="line"><span class="ln">212</span><span class="cl"><span class="s2">
</span></span></span><span class="line"><span class="ln">213</span><span class="cl"><span class="s2">        Returns:
</span></span></span><span class="line"><span class="ln">214</span><span class="cl"><span class="s2">            Dict with hits, misses, hit_rate, size, pinned, evictions
</span></span></span><span class="line"><span class="ln">215</span><span class="cl"><span class="s2">        &#34;&#34;&#34;</span>
</span></span><span class="line"><span class="ln">216</span><span class="cl">        <span class="n">total</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_hits</span> <span class="o">+</span> <span class="bp">self</span><span class="o">.</span><span class="n">_misses</span>
</span></span><span class="line"><span class="ln">217</span><span class="cl">        <span class="n">hit_rate</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_hits</span> <span class="o">/</span> <span class="n">total</span> <span class="k">if</span> <span class="n">total</span> <span class="o">&gt;</span> <span class="mi">0</span> <span class="k">else</span> <span class="mf">0.0</span>
</span></span><span class="line"><span class="ln">218</span><span class="cl">
</span></span><span class="line"><span class="ln">219</span><span class="cl">        <span class="k">return</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">220</span><span class="cl">            <span class="s2">&#34;hits&#34;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">_hits</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">221</span><span class="cl">            <span class="s2">&#34;misses&#34;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">_misses</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">222</span><span class="cl">            <span class="s2">&#34;hit_rate&#34;</span><span class="p">:</span> <span class="sa">f</span><span class="s2">&#34;</span><span class="si">{</span><span class="n">hit_rate</span><span class="si">:</span><span class="s2">.1%</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">223</span><span class="cl">            <span class="s2">&#34;total_size&#34;</span><span class="p">:</span> <span class="nb">len</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">_cache</span><span class="p">)</span> <span class="o">+</span> <span class="nb">len</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">_strong_refs</span><span class="p">),</span>
</span></span><span class="line"><span class="ln">224</span><span class="cl">            <span class="s2">&#34;weak_refs&#34;</span><span class="p">:</span> <span class="nb">len</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">_cache</span><span class="p">),</span>
</span></span><span class="line"><span class="ln">225</span><span class="cl">            <span class="s2">&#34;pinned&#34;</span><span class="p">:</span> <span class="nb">len</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">_strong_refs</span><span class="p">),</span>
</span></span><span class="line"><span class="ln">226</span><span class="cl">            <span class="s2">&#34;evictions&#34;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">_evictions</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">227</span><span class="cl">        <span class="p">}</span>
</span></span><span class="line"><span class="ln">228</span><span class="cl">
</span></span><span class="line"><span class="ln">229</span><span class="cl">    <span class="k">def</span> <span class="nf">memory_usage</span><span class="p">(</span><span class="bp">self</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">dict</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">230</span><span class="cl">        <span class="s2">&#34;&#34;&#34;
</span></span></span><span class="line"><span class="ln">231</span><span class="cl"><span class="s2">        Estimate memory usage of cached entries.
</span></span></span><span class="line"><span class="ln">232</span><span class="cl"><span class="s2">
</span></span></span><span class="line"><span class="ln">233</span><span class="cl"><span class="s2">        Returns:
</span></span></span><span class="line"><span class="ln">234</span><span class="cl"><span class="s2">            Dict with entry_count, estimated_bytes, per_entry_bytes
</span></span></span><span class="line"><span class="ln">235</span><span class="cl"><span class="s2">        &#34;&#34;&#34;</span>
</span></span><span class="line"><span class="ln">236</span><span class="cl">        <span class="n">all_entries</span> <span class="o">=</span> <span class="nb">list</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">_cache</span><span class="o">.</span><span class="n">values</span><span class="p">())</span> <span class="o">+</span> <span class="nb">list</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">_strong_refs</span><span class="o">.</span><span class="n">values</span><span class="p">())</span>
</span></span><span class="line"><span class="ln">237</span><span class="cl">
</span></span><span class="line"><span class="ln">238</span><span class="cl">        <span class="k">if</span> <span class="ow">not</span> <span class="n">all_entries</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">239</span><span class="cl">            <span class="k">return</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">240</span><span class="cl">                <span class="s2">&#34;entry_count&#34;</span><span class="p">:</span> <span class="mi">0</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">241</span><span class="cl">                <span class="s2">&#34;estimated_bytes&#34;</span><span class="p">:</span> <span class="mi">0</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">242</span><span class="cl">                <span class="s2">&#34;per_entry_bytes&#34;</span><span class="p">:</span> <span class="mi">0</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">243</span><span class="cl">            <span class="p">}</span>
</span></span><span class="line"><span class="ln">244</span><span class="cl">
</span></span><span class="line"><span class="ln">245</span><span class="cl">        <span class="c1"># Estimate based on first entry</span>
</span></span><span class="line"><span class="ln">246</span><span class="cl">        <span class="n">sample</span> <span class="o">=</span> <span class="n">all_entries</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span>
</span></span><span class="line"><span class="ln">247</span><span class="cl">        <span class="n">per_entry</span> <span class="o">=</span> <span class="n">sys</span><span class="o">.</span><span class="n">getsizeof</span><span class="p">(</span><span class="n">sample</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">248</span><span class="cl">
</span></span><span class="line"><span class="ln">249</span><span class="cl">        <span class="k">return</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">250</span><span class="cl">            <span class="s2">&#34;entry_count&#34;</span><span class="p">:</span> <span class="nb">len</span><span class="p">(</span><span class="n">all_entries</span><span class="p">),</span>
</span></span><span class="line"><span class="ln">251</span><span class="cl">            <span class="s2">&#34;estimated_bytes&#34;</span><span class="p">:</span> <span class="n">per_entry</span> <span class="o">*</span> <span class="nb">len</span><span class="p">(</span><span class="n">all_entries</span><span class="p">),</span>
</span></span><span class="line"><span class="ln">252</span><span class="cl">            <span class="s2">&#34;per_entry_bytes&#34;</span><span class="p">:</span> <span class="n">per_entry</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">253</span><span class="cl">        <span class="p">}</span>
</span></span><span class="line"><span class="ln">254</span><span class="cl">
</span></span><span class="line"><span class="ln">255</span><span class="cl"><span class="k">def</span> <span class="nf">demo_memory_optimization</span><span class="p">():</span>
</span></span><span class="line"><span class="ln">256</span><span class="cl">    <span class="s2">&#34;&#34;&#34;Demonstrate memory optimization techniques&#34;&#34;&#34;</span>
</span></span><span class="line"><span class="ln">257</span><span class="cl">
</span></span><span class="line"><span class="ln">258</span><span class="cl">    <span class="nb">print</span><span class="p">(</span><span class="s2">&#34;=&#34;</span> <span class="o">*</span> <span class="mi">60</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">259</span><span class="cl">    <span class="nb">print</span><span class="p">(</span><span class="s2">&#34;Memory Optimization Demo&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">260</span><span class="cl">    <span class="nb">print</span><span class="p">(</span><span class="s2">&#34;=&#34;</span> <span class="o">*</span> <span class="mi">60</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">261</span><span class="cl">
</span></span><span class="line"><span class="ln">262</span><span class="cl">    <span class="c1"># Start memory tracking</span>
</span></span><span class="line"><span class="ln">263</span><span class="cl">    <span class="n">tracemalloc</span><span class="o">.</span><span class="n">start</span><span class="p">()</span>
</span></span><span class="line"><span class="ln">264</span><span class="cl">    <span class="n">gc</span><span class="o">.</span><span class="n">collect</span><span class="p">()</span>
</span></span><span class="line"><span class="ln">265</span><span class="cl">    <span class="n">snapshot1</span> <span class="o">=</span> <span class="n">tracemalloc</span><span class="o">.</span><span class="n">take_snapshot</span><span class="p">()</span>
</span></span><span class="line"><span class="ln">266</span><span class="cl">
</span></span><span class="line"><span class="ln">267</span><span class="cl">    <span class="c1"># Create cache and populate</span>
</span></span><span class="line"><span class="ln">268</span><span class="cl">    <span class="n">cache</span> <span class="o">=</span> <span class="n">SmartConfigCache</span><span class="p">(</span><span class="n">max_size</span><span class="o">=</span><span class="mi">100</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">269</span><span class="cl">
</span></span><span class="line"><span class="ln">270</span><span class="cl">    <span class="c1"># Simulate loading many configurations</span>
</span></span><span class="line"><span class="ln">271</span><span class="cl">    <span class="n">entries</span> <span class="o">=</span> <span class="p">[]</span>
</span></span><span class="line"><span class="ln">272</span><span class="cl">    <span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="mi">1000</span><span class="p">):</span>
</span></span><span class="line"><span class="ln">273</span><span class="cl">        <span class="n">entry</span> <span class="o">=</span> <span class="n">cache</span><span class="o">.</span><span class="n">get_or_create</span><span class="p">(</span>
</span></span><span class="line"><span class="ln">274</span><span class="cl">            <span class="sa">f</span><span class="s2">&#34;config.item.</span><span class="si">{</span><span class="n">i</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">275</span><span class="cl">            <span class="k">lambda</span> <span class="n">i</span><span class="o">=</span><span class="n">i</span><span class="p">:</span> <span class="n">ConfigEntry</span><span class="p">(</span>
</span></span><span class="line"><span class="ln">276</span><span class="cl">                <span class="sa">f</span><span class="s2">&#34;config.item.</span><span class="si">{</span><span class="n">i</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">277</span><span class="cl">                <span class="sa">f</span><span class="s2">&#34;value_</span><span class="si">{</span><span class="n">i</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">278</span><span class="cl">                <span class="s2">&#34;demo&#34;</span>
</span></span><span class="line"><span class="ln">279</span><span class="cl">            <span class="p">),</span>
</span></span><span class="line"><span class="ln">280</span><span class="cl">            <span class="n">keep_strong</span><span class="o">=</span><span class="p">(</span><span class="n">i</span> <span class="o">&lt;</span> <span class="mi">100</span><span class="p">)</span>  <span class="c1"># Pin first 100</span>
</span></span><span class="line"><span class="ln">281</span><span class="cl">        <span class="p">)</span>
</span></span><span class="line"><span class="ln">282</span><span class="cl">        <span class="n">entries</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="n">entry</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">283</span><span class="cl">
</span></span><span class="line"><span class="ln">284</span><span class="cl">    <span class="c1"># Take snapshot after creation</span>
</span></span><span class="line"><span class="ln">285</span><span class="cl">    <span class="n">gc</span><span class="o">.</span><span class="n">collect</span><span class="p">()</span>
</span></span><span class="line"><span class="ln">286</span><span class="cl">    <span class="n">snapshot2</span> <span class="o">=</span> <span class="n">tracemalloc</span><span class="o">.</span><span class="n">take_snapshot</span><span class="p">()</span>
</span></span><span class="line"><span class="ln">287</span><span class="cl">
</span></span><span class="line"><span class="ln">288</span><span class="cl">    <span class="c1"># Print stats</span>
</span></span><span class="line"><span class="ln">289</span><span class="cl">    <span class="nb">print</span><span class="p">(</span><span class="s2">&#34;</span><span class="se">\n</span><span class="s2">Cache Statistics:&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">290</span><span class="cl">    <span class="k">for</span> <span class="n">key</span><span class="p">,</span> <span class="n">value</span> <span class="ow">in</span> <span class="n">cache</span><span class="o">.</span><span class="n">stats</span><span class="p">()</span><span class="o">.</span><span class="n">items</span><span class="p">():</span>
</span></span><span class="line"><span class="ln">291</span><span class="cl">        <span class="nb">print</span><span class="p">(</span><span class="sa">f</span><span class="s2">&#34;  </span><span class="si">{</span><span class="n">key</span><span class="si">}</span><span class="s2">: </span><span class="si">{</span><span class="n">value</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">292</span><span class="cl">
</span></span><span class="line"><span class="ln">293</span><span class="cl">    <span class="nb">print</span><span class="p">(</span><span class="s2">&#34;</span><span class="se">\n</span><span class="s2">Memory Usage:&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">294</span><span class="cl">    <span class="k">for</span> <span class="n">key</span><span class="p">,</span> <span class="n">value</span> <span class="ow">in</span> <span class="n">cache</span><span class="o">.</span><span class="n">memory_usage</span><span class="p">()</span><span class="o">.</span><span class="n">items</span><span class="p">():</span>
</span></span><span class="line"><span class="ln">295</span><span class="cl">        <span class="nb">print</span><span class="p">(</span><span class="sa">f</span><span class="s2">&#34;  </span><span class="si">{</span><span class="n">key</span><span class="si">}</span><span class="s2">: </span><span class="si">{</span><span class="n">value</span><span class="si">:</span><span class="s2">,</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">296</span><span class="cl">
</span></span><span class="line"><span class="ln">297</span><span class="cl">    <span class="c1"># Show memory diff</span>
</span></span><span class="line"><span class="ln">298</span><span class="cl">    <span class="n">diff</span> <span class="o">=</span> <span class="n">snapshot2</span><span class="o">.</span><span class="n">compare_to</span><span class="p">(</span><span class="n">snapshot1</span><span class="p">,</span> <span class="s1">&#39;lineno&#39;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">299</span><span class="cl">    <span class="nb">print</span><span class="p">(</span><span class="s2">&#34;</span><span class="se">\n</span><span class="s2">Top 5 Memory Allocations:&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">300</span><span class="cl">    <span class="k">for</span> <span class="n">stat</span> <span class="ow">in</span> <span class="n">diff</span><span class="p">[:</span><span class="mi">5</span><span class="p">]:</span>
</span></span><span class="line"><span class="ln">301</span><span class="cl">        <span class="nb">print</span><span class="p">(</span><span class="sa">f</span><span class="s2">&#34;  </span><span class="si">{</span><span class="n">stat</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">302</span><span class="cl">
</span></span><span class="line"><span class="ln">303</span><span class="cl">    <span class="c1"># Demo weak reference cleanup</span>
</span></span><span class="line"><span class="ln">304</span><span class="cl">    <span class="nb">print</span><span class="p">(</span><span class="s2">&#34;</span><span class="se">\n</span><span class="s2">&#34;</span> <span class="o">+</span> <span class="s2">&#34;-&#34;</span> <span class="o">*</span> <span class="mi">60</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">305</span><span class="cl">    <span class="nb">print</span><span class="p">(</span><span class="s2">&#34;Weak Reference Cleanup Demo&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">306</span><span class="cl">    <span class="nb">print</span><span class="p">(</span><span class="s2">&#34;-&#34;</span> <span class="o">*</span> <span class="mi">60</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">307</span><span class="cl">
</span></span><span class="line"><span class="ln">308</span><span class="cl">    <span class="nb">print</span><span class="p">(</span><span class="sa">f</span><span class="s2">&#34;Before cleanup - Cache size: </span><span class="si">{</span><span class="n">cache</span><span class="o">.</span><span class="n">stats</span><span class="p">()[</span><span class="s1">&#39;total_size&#39;</span><span class="p">]</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">309</span><span class="cl">
</span></span><span class="line"><span class="ln">310</span><span class="cl">    <span class="c1"># Delete external references to unpinned entries</span>
</span></span><span class="line"><span class="ln">311</span><span class="cl">    <span class="k">del</span> <span class="n">entries</span>
</span></span><span class="line"><span class="ln">312</span><span class="cl">    <span class="n">gc</span><span class="o">.</span><span class="n">collect</span><span class="p">()</span>
</span></span><span class="line"><span class="ln">313</span><span class="cl">
</span></span><span class="line"><span class="ln">314</span><span class="cl">    <span class="nb">print</span><span class="p">(</span><span class="sa">f</span><span class="s2">&#34;After cleanup  - Cache size: </span><span class="si">{</span><span class="n">cache</span><span class="o">.</span><span class="n">stats</span><span class="p">()[</span><span class="s1">&#39;total_size&#39;</span><span class="p">]</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">315</span><span class="cl">    <span class="nb">print</span><span class="p">(</span><span class="s2">&#34;(Only pinned entries remain)&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">316</span><span class="cl">
</span></span><span class="line"><span class="ln">317</span><span class="cl">    <span class="n">tracemalloc</span><span class="o">.</span><span class="n">stop</span><span class="p">()</span>
</span></span><span class="line"><span class="ln">318</span><span class="cl">
</span></span><span class="line"><span class="ln">319</span><span class="cl"><span class="k">if</span> <span class="vm">__name__</span> <span class="o">==</span> <span class="s2">&#34;__main__&#34;</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">320</span><span class="cl">    <span class="n">demo_memory_optimization</span><span class="p">()</span></span></span></code></pre></div><hr>
<h3 id="使用範例">使用範例</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="kn">from</span> <span class="nn">memory_optimized_cache</span> <span class="kn">import</span> <span class="n">SmartConfigCache</span><span class="p">,</span> <span class="n">ConfigEntry</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="c1"># Initialize cache with size limit</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="n">cache</span> <span class="o">=</span> <span class="n">SmartConfigCache</span><span class="p">(</span><span class="n">max_size</span><span class="o">=</span><span class="mi">1000</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="c1"># Load configuration</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="k">def</span> <span class="nf">load_database_config</span><span class="p">():</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">    <span class="s2">&#34;&#34;&#34;Factory function to load database config&#34;&#34;&#34;</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">    <span class="k">return</span> <span class="n">ConfigEntry</span><span class="p">(</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">        <span class="n">key</span><span class="o">=</span><span class="s2">&#34;database&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">        <span class="n">value</span><span class="o">=</span><span class="p">{</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">            <span class="s2">&#34;host&#34;</span><span class="p">:</span> <span class="s2">&#34;localhost&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">            <span class="s2">&#34;port&#34;</span><span class="p">:</span> <span class="mi">5432</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">            <span class="s2">&#34;name&#34;</span><span class="p">:</span> <span class="s2">&#34;myapp&#34;</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">        <span class="p">},</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">        <span class="n">source</span><span class="o">=</span><span class="s2">&#34;config.yaml&#34;</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl">    <span class="p">)</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl">
</span></span><span class="line"><span class="ln">19</span><span class="cl"><span class="c1"># Get or create (with strong reference for important config)</span>
</span></span><span class="line"><span class="ln">20</span><span class="cl"><span class="n">db_config</span> <span class="o">=</span> <span class="n">cache</span><span class="o">.</span><span class="n">get_or_create</span><span class="p">(</span>
</span></span><span class="line"><span class="ln">21</span><span class="cl">    <span class="s2">&#34;database&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">22</span><span class="cl">    <span class="n">load_database_config</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">23</span><span class="cl">    <span class="n">keep_strong</span><span class="o">=</span><span class="kc">True</span>  <span class="c1"># Keep in memory</span>
</span></span><span class="line"><span class="ln">24</span><span class="cl"><span class="p">)</span>
</span></span><span class="line"><span class="ln">25</span><span class="cl">
</span></span><span class="line"><span class="ln">26</span><span class="cl"><span class="nb">print</span><span class="p">(</span><span class="sa">f</span><span class="s2">&#34;Database host: </span><span class="si">{</span><span class="n">db_config</span><span class="o">.</span><span class="n">value</span><span class="p">[</span><span class="s1">&#39;host&#39;</span><span class="p">]</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">27</span><span class="cl">
</span></span><span class="line"><span class="ln">28</span><span class="cl"><span class="c1"># Temporary config (will be auto-cleaned when not referenced)</span>
</span></span><span class="line"><span class="ln">29</span><span class="cl"><span class="n">temp_config</span> <span class="o">=</span> <span class="n">cache</span><span class="o">.</span><span class="n">get_or_create</span><span class="p">(</span>
</span></span><span class="line"><span class="ln">30</span><span class="cl">    <span class="s2">&#34;temp.setting&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">31</span><span class="cl">    <span class="k">lambda</span><span class="p">:</span> <span class="n">ConfigEntry</span><span class="p">(</span><span class="s2">&#34;temp.setting&#34;</span><span class="p">,</span> <span class="s2">&#34;temporary&#34;</span><span class="p">,</span> <span class="s2">&#34;runtime&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">32</span><span class="cl"><span class="p">)</span>
</span></span><span class="line"><span class="ln">33</span><span class="cl">
</span></span><span class="line"><span class="ln">34</span><span class="cl"><span class="c1"># Check statistics</span>
</span></span><span class="line"><span class="ln">35</span><span class="cl"><span class="nb">print</span><span class="p">(</span><span class="n">cache</span><span class="o">.</span><span class="n">stats</span><span class="p">())</span>
</span></span><span class="line"><span class="ln">36</span><span class="cl"><span class="c1"># {&#39;hits&#39;: 0, &#39;misses&#39;: 2, &#39;hit_rate&#39;: &#39;0.0%&#39;,</span>
</span></span><span class="line"><span class="ln">37</span><span class="cl"><span class="c1">#  &#39;total_size&#39;: 2, &#39;weak_refs&#39;: 1, &#39;pinned&#39;: 1, &#39;evictions&#39;: 0}</span>
</span></span><span class="line"><span class="ln">38</span><span class="cl">
</span></span><span class="line"><span class="ln">39</span><span class="cl"><span class="c1"># Memory usage</span>
</span></span><span class="line"><span class="ln">40</span><span class="cl"><span class="nb">print</span><span class="p">(</span><span class="n">cache</span><span class="o">.</span><span class="n">memory_usage</span><span class="p">())</span>
</span></span><span class="line"><span class="ln">41</span><span class="cl"><span class="c1"># {&#39;entry_count&#39;: 2, &#39;estimated_bytes&#39;: 112, &#39;per_entry_bytes&#39;: 56}</span></span></span></code></pre></div><hr>
<h2 id="設計權衡">設計權衡</h2>
<h3 id="__slots__-vs-標準類別"><code>__slots__</code> vs 標準類別</h3>
<table>
  <thead>
      <tr>
          <th>面向</th>
          <th>標準類別</th>
          <th><code>__slots__</code></th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><strong>記憶體佔用</strong></td>
          <td>較多（有 <code>__dict__</code>）</td>
          <td>較少（節省 ~60-70%）</td>
      </tr>
      <tr>
          <td><strong>動態屬性</strong></td>
          <td>支援 <code>obj.new_attr = x</code></td>
          <td>不支援（除非加 <code>__dict__</code>）</td>
      </tr>
      <tr>
          <td><strong>繼承</strong></td>
          <td>簡單</td>
          <td>子類別需要自己的 <code>__slots__</code></td>
      </tr>
      <tr>
          <td><strong>弱引用</strong></td>
          <td>預設支援</td>
          <td>需要加入 <code>__weakref__</code> slot</td>
      </tr>
      <tr>
          <td><strong>Pickle</strong></td>
          <td>直接支援</td>
          <td>需要 <code>__getstate__</code>/<code>__setstate__</code></td>
      </tr>
      <tr>
          <td><strong>多重繼承</strong></td>
          <td>正常運作</td>
          <td>多個父類別不能都有非空 <code>__slots__</code></td>
      </tr>
  </tbody>
</table>
<h3 id="強引用-vs-弱引用快取">強引用 vs 弱引用快取</h3>
<table>
  <thead>
      <tr>
          <th>面向</th>
          <th>強引用快取</th>
          <th>弱引用快取</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><strong>記憶體管理</strong></td>
          <td>需要手動清理</td>
          <td>自動清理</td>
      </tr>
      <tr>
          <td><strong>資料保證</strong></td>
          <td>資料一定存在</td>
          <td>資料可能被回收</td>
      </tr>
      <tr>
          <td><strong>適用場景</strong></td>
          <td>關鍵配置</td>
          <td>暫時性資料</td>
      </tr>
      <tr>
          <td><strong>實作複雜度</strong></td>
          <td>簡單</td>
          <td>稍微複雜</td>
      </tr>
  </tbody>
</table>
<h3 id="何時使用哪種技術">何時使用哪種技術？</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln"> 1</span><span class="cl">決策樹：
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">物件數量多嗎？
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">├── 是 → 考慮 __slots__
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">│   └── 需要動態屬性嗎？
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">│       ├── 是 → __slots__ = [..., &#39;__dict__&#39;]
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">│       └── 否 → __slots__ = [...]
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">└── 否 → 標準類別即可
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">
</span></span><span class="line"><span class="ln">10</span><span class="cl">快取可能無限增長嗎？
</span></span><span class="line"><span class="ln">11</span><span class="cl">├── 是 → 使用 WeakValueDictionary 或 LRU
</span></span><span class="line"><span class="ln">12</span><span class="cl">└── 否 → 普通字典即可
</span></span><span class="line"><span class="ln">13</span><span class="cl">
</span></span><span class="line"><span class="ln">14</span><span class="cl">資料可以被回收嗎？
</span></span><span class="line"><span class="ln">15</span><span class="cl">├── 是 → weakref
</span></span><span class="line"><span class="ln">16</span><span class="cl">└── 否 → 強引用</span></span></code></pre></div><hr>
<h2 id="什麼時候該用這個技術">什麼時候該用這個技術？</h2>
<h3 id="適合使用">適合使用</h3>
<ul>
<li><strong>建立大量小物件</strong>：如資料點、事件、配置項目</li>
<li><strong>記憶體使用是瓶頸</strong>：經過 profiling 確認</li>
<li><strong>快取可能無限增長</strong>：如用戶 session、請求資料</li>
<li><strong>長時間運行的服務</strong>：如 web server、daemon</li>
</ul>
<h3 id="不建議使用">不建議使用</h3>
<ul>
<li><strong>物件數量很少</strong>：優化效果不明顯</li>
<li><strong>需要動態新增屬性</strong>：<code>__slots__</code> 會限制彈性</li>
<li><strong>過早優化</strong>：先確認是否真的有問題</li>
<li><strong>程式碼可讀性優先</strong>：標準類別更直觀</li>
</ul>
<h3 id="優化決策流程">優化決策流程</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1"># Step 1: Profile first!</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1"># Don&#39;t optimize until you know where the problem is</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="kn">import</span> <span class="nn">tracemalloc</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="n">tracemalloc</span><span class="o">.</span><span class="n">start</span><span class="p">()</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="c1"># ... run your code ...</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="n">snapshot</span> <span class="o">=</span> <span class="n">tracemalloc</span><span class="o">.</span><span class="n">take_snapshot</span><span class="p">()</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="n">top_stats</span> <span class="o">=</span> <span class="n">snapshot</span><span class="o">.</span><span class="n">statistics</span><span class="p">(</span><span class="s1">&#39;lineno&#39;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="k">for</span> <span class="n">stat</span> <span class="ow">in</span> <span class="n">top_stats</span><span class="p">[:</span><span class="mi">10</span><span class="p">]:</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">    <span class="nb">print</span><span class="p">(</span><span class="n">stat</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">
</span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="c1"># Step 2: If memory is the issue, identify the class</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="c1"># Look for classes with many instances</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">
</span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="kn">import</span> <span class="nn">gc</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="kn">from</span> <span class="nn">collections</span> <span class="kn">import</span> <span class="n">Counter</span>
</span></span><span class="line"><span class="ln">19</span><span class="cl">
</span></span><span class="line"><span class="ln">20</span><span class="cl"><span class="n">counter</span> <span class="o">=</span> <span class="n">Counter</span><span class="p">(</span><span class="nb">type</span><span class="p">(</span><span class="n">obj</span><span class="p">)</span><span class="o">.</span><span class="vm">__name__</span> <span class="k">for</span> <span class="n">obj</span> <span class="ow">in</span> <span class="n">gc</span><span class="o">.</span><span class="n">get_objects</span><span class="p">())</span>
</span></span><span class="line"><span class="ln">21</span><span class="cl"><span class="nb">print</span><span class="p">(</span><span class="n">counter</span><span class="o">.</span><span class="n">most_common</span><span class="p">(</span><span class="mi">10</span><span class="p">))</span>
</span></span><span class="line"><span class="ln">22</span><span class="cl">
</span></span><span class="line"><span class="ln">23</span><span class="cl"><span class="c1"># Step 3: Only then apply __slots__ to hot classes</span></span></span></code></pre></div><hr>
<h2 id="練習">練習</h2>
<h3 id="基礎練習比較有無-__slots__-的記憶體差異">基礎練習：比較有無 <code>__slots__</code> 的記憶體差異</h3>
<p>撰寫一個腳本，比較以下三種類別建立 100,000 個實例的記憶體使用：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1"># Exercise: Complete this benchmark script</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="kn">import</span> <span class="nn">sys</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="kn">import</span> <span class="nn">tracemalloc</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="kn">from</span> <span class="nn">dataclasses</span> <span class="kn">import</span> <span class="n">dataclass</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="c1"># 1. Standard class</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="k">class</span> <span class="nc">PointStandard</span><span class="p">:</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">    <span class="k">def</span> <span class="fm">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">x</span><span class="p">,</span> <span class="n">y</span><span class="p">,</span> <span class="n">z</span><span class="p">):</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">x</span> <span class="o">=</span> <span class="n">x</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">y</span> <span class="o">=</span> <span class="n">y</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">z</span> <span class="o">=</span> <span class="n">z</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">
</span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="c1"># 2. Class with __slots__</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="k">class</span> <span class="nc">PointSlots</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">    <span class="vm">__slots__</span> <span class="o">=</span> <span class="p">[</span><span class="s1">&#39;x&#39;</span><span class="p">,</span> <span class="s1">&#39;y&#39;</span><span class="p">,</span> <span class="s1">&#39;z&#39;</span><span class="p">]</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl">    <span class="k">def</span> <span class="fm">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">x</span><span class="p">,</span> <span class="n">y</span><span class="p">,</span> <span class="n">z</span><span class="p">):</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">x</span> <span class="o">=</span> <span class="n">x</span>
</span></span><span class="line"><span class="ln">19</span><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">y</span> <span class="o">=</span> <span class="n">y</span>
</span></span><span class="line"><span class="ln">20</span><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">z</span> <span class="o">=</span> <span class="n">z</span>
</span></span><span class="line"><span class="ln">21</span><span class="cl">
</span></span><span class="line"><span class="ln">22</span><span class="cl"><span class="c1"># 3. Named tuple</span>
</span></span><span class="line"><span class="ln">23</span><span class="cl"><span class="kn">from</span> <span class="nn">collections</span> <span class="kn">import</span> <span class="n">namedtuple</span>
</span></span><span class="line"><span class="ln">24</span><span class="cl"><span class="n">PointNamed</span> <span class="o">=</span> <span class="n">namedtuple</span><span class="p">(</span><span class="s1">&#39;PointNamed&#39;</span><span class="p">,</span> <span class="p">[</span><span class="s1">&#39;x&#39;</span><span class="p">,</span> <span class="s1">&#39;y&#39;</span><span class="p">,</span> <span class="s1">&#39;z&#39;</span><span class="p">])</span>
</span></span><span class="line"><span class="ln">25</span><span class="cl">
</span></span><span class="line"><span class="ln">26</span><span class="cl"><span class="c1"># TODO: Write benchmark function</span>
</span></span><span class="line"><span class="ln">27</span><span class="cl"><span class="k">def</span> <span class="nf">benchmark</span><span class="p">(</span><span class="bp">cls</span><span class="p">,</span> <span class="n">count</span><span class="o">=</span><span class="mi">100000</span><span class="p">):</span>
</span></span><span class="line"><span class="ln">28</span><span class="cl">    <span class="s2">&#34;&#34;&#34;Measure memory for creating `count` instances&#34;&#34;&#34;</span>
</span></span><span class="line"><span class="ln">29</span><span class="cl">    <span class="k">pass</span>  <span class="c1"># Implement this</span>
</span></span><span class="line"><span class="ln">30</span><span class="cl">
</span></span><span class="line"><span class="ln">31</span><span class="cl"><span class="c1"># TODO: Compare results</span>
</span></span><span class="line"><span class="ln">32</span><span class="cl"><span class="c1"># Expected: PointSlots uses ~3x less memory than PointStandard</span></span></span></code></pre></div><h3 id="進階練習實作-weakref-快取">進階練習：實作 weakref 快取</h3>
<p>建立一個 <code>ImageCache</code> 類別，具有以下功能：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1"># Exercise: Implement ImageCache</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="k">class</span> <span class="nc">ImageCache</span><span class="p">:</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">    <span class="s2">&#34;&#34;&#34;
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="s2">    Cache for image data with automatic cleanup.
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="s2">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="s2">    Requirements:
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="s2">    - Use WeakValueDictionary for auto-cleanup
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="s2">    - Track hit/miss statistics
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="s2">    - Support maximum size limit
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="s2">    - Provide memory usage estimation
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="s2">
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="s2">    Example usage:
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="s2">        cache = ImageCache(max_size=100)
</span></span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="s2">
</span></span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="s2">        img = cache.get_or_load(&#34;photo.jpg&#34;, lambda: load_image(&#34;photo.jpg&#34;))
</span></span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="s2">
</span></span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="s2">        print(cache.stats())
</span></span></span><span class="line"><span class="ln">19</span><span class="cl"><span class="s2">        # {&#39;hits&#39;: 0, &#39;misses&#39;: 1, &#39;size&#39;: 1}
</span></span></span><span class="line"><span class="ln">20</span><span class="cl"><span class="s2">    &#34;&#34;&#34;</span>
</span></span><span class="line"><span class="ln">21</span><span class="cl">
</span></span><span class="line"><span class="ln">22</span><span class="cl">    <span class="k">def</span> <span class="fm">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">max_size</span><span class="o">=</span><span class="kc">None</span><span class="p">):</span>
</span></span><span class="line"><span class="ln">23</span><span class="cl">        <span class="c1"># TODO: Initialize cache</span>
</span></span><span class="line"><span class="ln">24</span><span class="cl">        <span class="k">pass</span>
</span></span><span class="line"><span class="ln">25</span><span class="cl">
</span></span><span class="line"><span class="ln">26</span><span class="cl">    <span class="k">def</span> <span class="nf">get_or_load</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">key</span><span class="p">,</span> <span class="n">loader</span><span class="p">):</span>
</span></span><span class="line"><span class="ln">27</span><span class="cl">        <span class="c1"># TODO: Implement get or load logic</span>
</span></span><span class="line"><span class="ln">28</span><span class="cl">        <span class="k">pass</span>
</span></span><span class="line"><span class="ln">29</span><span class="cl">
</span></span><span class="line"><span class="ln">30</span><span class="cl">    <span class="k">def</span> <span class="nf">stats</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
</span></span><span class="line"><span class="ln">31</span><span class="cl">        <span class="c1"># TODO: Return cache statistics</span>
</span></span><span class="line"><span class="ln">32</span><span class="cl">        <span class="k">pass</span></span></span></code></pre></div><h3 id="挑戰題用-tracemalloc-追蹤記憶體洩漏">挑戰題：用 tracemalloc 追蹤記憶體洩漏</h3>
<p>給定以下有記憶體洩漏的程式碼，使用 <code>tracemalloc</code> 找出問題並修復：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1"># Exercise: Find and fix the memory leak</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="k">class</span> <span class="nc">EventHandler</span><span class="p">:</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">    <span class="n">_handlers</span> <span class="o">=</span> <span class="p">[]</span>  <span class="c1"># Class variable - potential leak!</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">    <span class="k">def</span> <span class="fm">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">name</span><span class="p">):</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">name</span> <span class="o">=</span> <span class="n">name</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">callbacks</span> <span class="o">=</span> <span class="p">[]</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">        <span class="n">EventHandler</span><span class="o">.</span><span class="n">_handlers</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="bp">self</span><span class="p">)</span>  <span class="c1"># Leak: strong reference</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">
</span></span><span class="line"><span class="ln">11</span><span class="cl">    <span class="k">def</span> <span class="nf">register</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">callback</span><span class="p">):</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">callbacks</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="n">callback</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">
</span></span><span class="line"><span class="ln">14</span><span class="cl">    <span class="k">def</span> <span class="nf">fire</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">event</span><span class="p">):</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">        <span class="k">for</span> <span class="n">cb</span> <span class="ow">in</span> <span class="bp">self</span><span class="o">.</span><span class="n">callbacks</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">            <span class="n">cb</span><span class="p">(</span><span class="n">event</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl">
</span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="k">def</span> <span class="nf">process_events</span><span class="p">():</span>
</span></span><span class="line"><span class="ln">19</span><span class="cl">    <span class="s2">&#34;&#34;&#34;This function creates handlers but never cleans them up&#34;&#34;&#34;</span>
</span></span><span class="line"><span class="ln">20</span><span class="cl">    <span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="mi">1000</span><span class="p">):</span>
</span></span><span class="line"><span class="ln">21</span><span class="cl">        <span class="n">handler</span> <span class="o">=</span> <span class="n">EventHandler</span><span class="p">(</span><span class="sa">f</span><span class="s2">&#34;handler_</span><span class="si">{</span><span class="n">i</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">22</span><span class="cl">        <span class="n">handler</span><span class="o">.</span><span class="n">register</span><span class="p">(</span><span class="k">lambda</span> <span class="n">e</span><span class="p">:</span> <span class="nb">print</span><span class="p">(</span><span class="n">e</span><span class="p">))</span>
</span></span><span class="line"><span class="ln">23</span><span class="cl">        <span class="n">handler</span><span class="o">.</span><span class="n">fire</span><span class="p">(</span><span class="sa">f</span><span class="s2">&#34;event_</span><span class="si">{</span><span class="n">i</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">24</span><span class="cl">        <span class="c1"># handler goes out of scope but is still in _handlers!</span>
</span></span><span class="line"><span class="ln">25</span><span class="cl">
</span></span><span class="line"><span class="ln">26</span><span class="cl"><span class="c1"># TODO:</span>
</span></span><span class="line"><span class="ln">27</span><span class="cl"><span class="c1"># 1. Use tracemalloc to measure memory growth</span>
</span></span><span class="line"><span class="ln">28</span><span class="cl"><span class="c1"># 2. Identify the leak</span>
</span></span><span class="line"><span class="ln">29</span><span class="cl"><span class="c1"># 3. Fix EventHandler to use weak references</span>
</span></span><span class="line"><span class="ln">30</span><span class="cl"><span class="c1"># 4. Verify the fix with tracemalloc</span></span></span></code></pre></div><hr>
<h2 id="延伸閱讀">延伸閱讀</h2>
<ul>
<li><a href="https://docs.python.org/3/reference/datamodel.html#slots"><code>__slots__</code> 官方文件</a></li>
<li><a href="https://docs.python.org/3/library/weakref.html">weakref 官方文件</a></li>
<li><a href="https://docs.python.org/3/library/tracemalloc.html">tracemalloc 官方文件</a></li>
<li><a href="https://pympler.readthedocs.io/">Pympler - Memory profiling</a></li>
<li><a href="https://realpython.com/python-memory-management/">Python Memory Management - Real Python</a></li>
</ul>
<hr>
<p><em>上一章：<a href="/blog/python-advanced/04-cpython-internals/case-studies/profiling/" data-link-title="案例：效能分析實戰" data-link-desc="用 cProfile 和 line_profiler 分析 Markdown 連結檢查器的效能瓶頸">效能分析實戰</a></em>
<em>返回：<a href="/blog/python-advanced/04-cpython-internals/" data-link-title="模組四：CPython 內部機制" data-link-desc="深入 CPython 直譯器，理解 Python 如何運作">模組四：CPython 內部機制</a></em></p>
]]></content:encoded></item><item><title>3.2 記憶體管理與垃圾回收</title><link>https://tarrragon.github.io/blog/python-advanced/04-cpython-internals/memory-gc/</link><pubDate>Tue, 20 Jan 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/python-advanced/04-cpython-internals/memory-gc/</guid><description>&lt;p>Python 的記憶體管理結合了參考計數和分代垃圾回收。理解這些機制有助於寫出更高效的程式碼。&lt;/p>
&lt;h2 id="先備知識">先備知識&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/python-advanced/04-cpython-internals/object-model/" data-link-title="3.1 PyObject 與物件模型" data-link-desc="深入理解 Python 的物件模型">3.1 PyObject 與物件模型&lt;/a>&lt;/li>
&lt;/ul>
&lt;h2 id="本章目標">本章目標&lt;/h2>
&lt;p>學完本章後，你將能夠：&lt;/p>
&lt;ol>
&lt;li>理解參考計數的限制&lt;/li>
&lt;li>理解分代垃圾回收的原理&lt;/li>
&lt;li>使用 &lt;code>__slots__&lt;/code> 優化記憶體&lt;/li>
&lt;li>使用 &lt;code>tracemalloc&lt;/code> 分析記憶體使用&lt;/li>
&lt;/ol>
&lt;hr>
&lt;h2 id="原理層記憶體模型">【原理層】記憶體模型&lt;/h2>
&lt;h3 id="stack-vs-heap">Stack vs Heap&lt;/h3>
&lt;p>Python 的記憶體分為兩個主要區域：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">┌─────────────────────────────────────┐
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">│ Stack │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl">│ ┌─────────────────────────────────┐│
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl">│ │ 變數名稱 → 指向 Heap 的指標 ││
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl">│ │ a ──────→ [指標] ││
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">│ │ b ──────→ [指標] ││
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl">│ └─────────────────────────────────┘│
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">└─────────────────────────────────────┘
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl"> │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl"> ▼
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl">┌─────────────────────────────────────┐
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl">│ Heap │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">13&lt;/span>&lt;span class="cl">│ ┌─────────────────────────────────┐│
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">14&lt;/span>&lt;span class="cl">│ │ PyObject: [1, 2, 3] ││
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">15&lt;/span>&lt;span class="cl">│ │ PyObject: &amp;#34;hello&amp;#34; ││
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">16&lt;/span>&lt;span class="cl">│ │ PyObject: 42 ││
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">17&lt;/span>&lt;span class="cl">│ └─────────────────────────────────┘│
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">18&lt;/span>&lt;span class="cl">└─────────────────────────────────────┘&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;ul>
&lt;li>&lt;strong>Stack&lt;/strong>：儲存變數名稱和指標（參考）&lt;/li>
&lt;li>&lt;strong>Heap&lt;/strong>：儲存實際的 Python 物件&lt;/li>
&lt;/ul>
&lt;h3 id="python-的記憶體分配器">Python 的記憶體分配器&lt;/h3>
&lt;p>CPython 使用分層的記憶體分配器：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">┌─────────────────────────────────────┐
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">│ Python 物件分配器 │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl">│ (PyObject_Malloc) │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl">├─────────────────────────────────────┤
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl">│ Python 記憶體分配器 │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">│ (PyMem_Malloc) │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl">├─────────────────────────────────────┤
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">│ C 標準函式庫 │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">│ (malloc) │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">├─────────────────────────────────────┤
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl">│ 作業系統 │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl">└─────────────────────────────────────┘&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>對於小於 512 bytes 的物件，Python 使用自己的分配器來減少系統呼叫。&lt;/p>
&lt;hr>
&lt;h2 id="設計層循環參考問題">【設計層】循環參考問題&lt;/h2>
&lt;h3 id="參考計數的限制">參考計數的限制&lt;/h3>
&lt;p>參考計數無法處理循環參考：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="kn">import&lt;/span> &lt;span class="nn">gc&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl">&lt;span class="k">class&lt;/span> &lt;span class="nc">Node&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl"> &lt;span class="k">def&lt;/span> &lt;span class="fm">__init__&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="bp">self&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">name&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl"> &lt;span class="bp">self&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">name&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">name&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl"> &lt;span class="bp">self&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">ref&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="kc">None&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">&lt;span class="c1"># 建立循環參考&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">&lt;span class="n">a&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">Node&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;A&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">&lt;span class="n">b&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">Node&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;B&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl">&lt;span class="n">a&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">ref&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">b&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl">&lt;span class="n">b&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">ref&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">a&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">13&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">14&lt;/span>&lt;span class="cl">&lt;span class="c1"># 刪除外部參考&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">15&lt;/span>&lt;span class="cl">&lt;span class="k">del&lt;/span> &lt;span class="n">a&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">16&lt;/span>&lt;span class="cl">&lt;span class="k">del&lt;/span> &lt;span class="n">b&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">17&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">18&lt;/span>&lt;span class="cl">&lt;span class="c1"># 此時 A 和 B 仍互相參考，參考計數都是 1&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">19&lt;/span>&lt;span class="cl">&lt;span class="c1"># 但它們已經無法被存取了（垃圾）&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>




&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">刪除前：
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">外部 ─→ A ←──→ B ←─ 外部
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl"> refcnt=2 refcnt=2
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">刪除後：
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl"> A ←──→ B
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl"> refcnt=1 refcnt=1
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">8&lt;/span>&lt;span class="cl"> （無法被存取，但參考計數不為 0）&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="分代垃圾回收">分代垃圾回收&lt;/h3>
&lt;p>為了解決循環參考，Python 使用分代垃圾回收：&lt;/p></description><content:encoded><![CDATA[<p>Python 的記憶體管理結合了參考計數和分代垃圾回收。理解這些機制有助於寫出更高效的程式碼。</p>
<h2 id="先備知識">先備知識</h2>
<ul>
<li><a href="/blog/python-advanced/04-cpython-internals/object-model/" data-link-title="3.1 PyObject 與物件模型" data-link-desc="深入理解 Python 的物件模型">3.1 PyObject 與物件模型</a></li>
</ul>
<h2 id="本章目標">本章目標</h2>
<p>學完本章後，你將能夠：</p>
<ol>
<li>理解參考計數的限制</li>
<li>理解分代垃圾回收的原理</li>
<li>使用 <code>__slots__</code> 優化記憶體</li>
<li>使用 <code>tracemalloc</code> 分析記憶體使用</li>
</ol>
<hr>
<h2 id="原理層記憶體模型">【原理層】記憶體模型</h2>
<h3 id="stack-vs-heap">Stack vs Heap</h3>
<p>Python 的記憶體分為兩個主要區域：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln"> 1</span><span class="cl">┌─────────────────────────────────────┐
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">│              Stack                   │
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">│  ┌─────────────────────────────────┐│
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">│  │ 變數名稱 → 指向 Heap 的指標      ││
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">│  │ a ──────→ [指標]                ││
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">│  │ b ──────→ [指標]                ││
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">│  └─────────────────────────────────┘│
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">└─────────────────────────────────────┘
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">                 │
</span></span><span class="line"><span class="ln">10</span><span class="cl">                 ▼
</span></span><span class="line"><span class="ln">11</span><span class="cl">┌─────────────────────────────────────┐
</span></span><span class="line"><span class="ln">12</span><span class="cl">│              Heap                    │
</span></span><span class="line"><span class="ln">13</span><span class="cl">│  ┌─────────────────────────────────┐│
</span></span><span class="line"><span class="ln">14</span><span class="cl">│  │ PyObject: [1, 2, 3]             ││
</span></span><span class="line"><span class="ln">15</span><span class="cl">│  │ PyObject: &#34;hello&#34;               ││
</span></span><span class="line"><span class="ln">16</span><span class="cl">│  │ PyObject: 42                    ││
</span></span><span class="line"><span class="ln">17</span><span class="cl">│  └─────────────────────────────────┘│
</span></span><span class="line"><span class="ln">18</span><span class="cl">└─────────────────────────────────────┘</span></span></code></pre></div><ul>
<li><strong>Stack</strong>：儲存變數名稱和指標（參考）</li>
<li><strong>Heap</strong>：儲存實際的 Python 物件</li>
</ul>
<h3 id="python-的記憶體分配器">Python 的記憶體分配器</h3>
<p>CPython 使用分層的記憶體分配器：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln"> 1</span><span class="cl">┌─────────────────────────────────────┐
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">│     Python 物件分配器               │
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">│     (PyObject_Malloc)               │
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">├─────────────────────────────────────┤
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">│     Python 記憶體分配器             │
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">│     (PyMem_Malloc)                  │
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">├─────────────────────────────────────┤
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">│     C 標準函式庫                    │
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">│     (malloc)                        │
</span></span><span class="line"><span class="ln">10</span><span class="cl">├─────────────────────────────────────┤
</span></span><span class="line"><span class="ln">11</span><span class="cl">│     作業系統                        │
</span></span><span class="line"><span class="ln">12</span><span class="cl">└─────────────────────────────────────┘</span></span></code></pre></div><p>對於小於 512 bytes 的物件，Python 使用自己的分配器來減少系統呼叫。</p>
<hr>
<h2 id="設計層循環參考問題">【設計層】循環參考問題</h2>
<h3 id="參考計數的限制">參考計數的限制</h3>
<p>參考計數無法處理循環參考：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="kn">import</span> <span class="nn">gc</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="k">class</span> <span class="nc">Node</span><span class="p">:</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">    <span class="k">def</span> <span class="fm">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">name</span><span class="p">):</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">name</span> <span class="o">=</span> <span class="n">name</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">ref</span> <span class="o">=</span> <span class="kc">None</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="c1"># 建立循環參考</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="n">a</span> <span class="o">=</span> <span class="n">Node</span><span class="p">(</span><span class="s2">&#34;A&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="n">b</span> <span class="o">=</span> <span class="n">Node</span><span class="p">(</span><span class="s2">&#34;B&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="n">a</span><span class="o">.</span><span class="n">ref</span> <span class="o">=</span> <span class="n">b</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="n">b</span><span class="o">.</span><span class="n">ref</span> <span class="o">=</span> <span class="n">a</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">
</span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="c1"># 刪除外部參考</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="k">del</span> <span class="n">a</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="k">del</span> <span class="n">b</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl">
</span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="c1"># 此時 A 和 B 仍互相參考，參考計數都是 1</span>
</span></span><span class="line"><span class="ln">19</span><span class="cl"><span class="c1"># 但它們已經無法被存取了（垃圾）</span></span></span></code></pre></div>




<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">刪除前：
</span></span><span class="line"><span class="ln">2</span><span class="cl">外部 ─→ A ←──→ B ←─ 外部
</span></span><span class="line"><span class="ln">3</span><span class="cl">        refcnt=2  refcnt=2
</span></span><span class="line"><span class="ln">4</span><span class="cl">
</span></span><span class="line"><span class="ln">5</span><span class="cl">刪除後：
</span></span><span class="line"><span class="ln">6</span><span class="cl">        A ←──→ B
</span></span><span class="line"><span class="ln">7</span><span class="cl">        refcnt=1  refcnt=1
</span></span><span class="line"><span class="ln">8</span><span class="cl">        （無法被存取，但參考計數不為 0）</span></span></code></pre></div><h3 id="分代垃圾回收">分代垃圾回收</h3>
<p>為了解決循環參考，Python 使用分代垃圾回收：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="kn">import</span> <span class="nn">gc</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="c1"># 查看 GC 統計</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="nb">print</span><span class="p">(</span><span class="n">gc</span><span class="o">.</span><span class="n">get_count</span><span class="p">())</span>  <span class="c1"># (700, 10, 0) - 各代的物件數</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="c1"># 三個世代（Python 3.12 以前）</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="c1"># Generation 0: 新物件</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="c1"># Generation 1: 存活過一次 GC 的物件</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="c1"># Generation 2: 存活過多次 GC 的物件</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="c1"># Python 3.12+ 改為四個世代</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="c1"># Young generation (1 代)</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="c1"># Old generations (2 代)</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="c1"># Permanent generation (永久)</span></span></span></code></pre></div><h3 id="gc-觸發時機">GC 觸發時機</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="kn">import</span> <span class="nn">gc</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="c1"># 查看閾值</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="nb">print</span><span class="p">(</span><span class="n">gc</span><span class="o">.</span><span class="n">get_threshold</span><span class="p">())</span>  <span class="c1"># (700, 10, 10)</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="c1"># 意義：</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="c1"># - 當 Generation 0 有 700 個物件時，觸發 Gen 0 GC</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="c1"># - 當 Gen 0 GC 執行 10 次後，觸發 Gen 1 GC</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="c1"># - 當 Gen 1 GC 執行 10 次後，觸發 Gen 2 GC</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="c1"># 手動觸發 GC</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="n">gc</span><span class="o">.</span><span class="n">collect</span><span class="p">()</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">
</span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="c1"># 設定閾值</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="n">gc</span><span class="o">.</span><span class="n">set_threshold</span><span class="p">(</span><span class="mi">1000</span><span class="p">,</span> <span class="mi">15</span><span class="p">,</span> <span class="mi">15</span><span class="p">)</span></span></span></code></pre></div><hr>
<h2 id="實作層記憶體優化">【實作層】記憶體優化</h2>
<h3 id="使用-slots">使用 <strong>slots</strong></h3>
<p><code>__slots__</code> 可以顯著減少物件的記憶體使用：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="kn">import</span> <span class="nn">sys</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="k">class</span> <span class="nc">WithoutSlots</span><span class="p">:</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">    <span class="k">def</span> <span class="fm">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">x</span><span class="p">,</span> <span class="n">y</span><span class="p">):</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">x</span> <span class="o">=</span> <span class="n">x</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">y</span> <span class="o">=</span> <span class="n">y</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="k">class</span> <span class="nc">WithSlots</span><span class="p">:</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">    <span class="vm">__slots__</span> <span class="o">=</span> <span class="p">[</span><span class="s1">&#39;x&#39;</span><span class="p">,</span> <span class="s1">&#39;y&#39;</span><span class="p">]</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">
</span></span><span class="line"><span class="ln">11</span><span class="cl">    <span class="k">def</span> <span class="fm">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">x</span><span class="p">,</span> <span class="n">y</span><span class="p">):</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">x</span> <span class="o">=</span> <span class="n">x</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">y</span> <span class="o">=</span> <span class="n">y</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">
</span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="n">obj1</span> <span class="o">=</span> <span class="n">WithoutSlots</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="mi">2</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="n">obj2</span> <span class="o">=</span> <span class="n">WithSlots</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="mi">2</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl">
</span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="nb">print</span><span class="p">(</span><span class="n">sys</span><span class="o">.</span><span class="n">getsizeof</span><span class="p">(</span><span class="n">obj1</span><span class="p">))</span>  <span class="c1"># 48</span>
</span></span><span class="line"><span class="ln">19</span><span class="cl"><span class="nb">print</span><span class="p">(</span><span class="n">sys</span><span class="o">.</span><span class="n">getsizeof</span><span class="p">(</span><span class="n">obj2</span><span class="p">))</span>  <span class="c1"># 48（但沒有 __dict__）</span>
</span></span><span class="line"><span class="ln">20</span><span class="cl">
</span></span><span class="line"><span class="ln">21</span><span class="cl"><span class="c1"># 實際差異在 __dict__</span>
</span></span><span class="line"><span class="ln">22</span><span class="cl"><span class="nb">print</span><span class="p">(</span><span class="n">sys</span><span class="o">.</span><span class="n">getsizeof</span><span class="p">(</span><span class="n">obj1</span><span class="o">.</span><span class="vm">__dict__</span><span class="p">))</span>  <span class="c1"># 104</span>
</span></span><span class="line"><span class="ln">23</span><span class="cl"><span class="c1"># obj2 沒有 __dict__</span></span></span></code></pre></div><h4 id="為什麼-__slots__-省記憶體">為什麼 <code>__slots__</code> 省記憶體？</h4>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln"> 1</span><span class="cl">沒有 __slots__：
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">┌─────────────────────────┐
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">│ PyObject header (16 B)  │
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">│ __dict__ 指標 (8 B)      │
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">│ __weakref__ 指標 (8 B)   │
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">│ __dict__ → { &#39;x&#39;: 1,    │
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">│              &#39;y&#39;: 2 }   │
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">│            （額外 100+ B）│
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">└─────────────────────────┘
</span></span><span class="line"><span class="ln">10</span><span class="cl">
</span></span><span class="line"><span class="ln">11</span><span class="cl">有 __slots__：
</span></span><span class="line"><span class="ln">12</span><span class="cl">┌─────────────────────────┐
</span></span><span class="line"><span class="ln">13</span><span class="cl">│ PyObject header (16 B)  │
</span></span><span class="line"><span class="ln">14</span><span class="cl">│ x (8 B)                 │
</span></span><span class="line"><span class="ln">15</span><span class="cl">│ y (8 B)                 │
</span></span><span class="line"><span class="ln">16</span><span class="cl">└─────────────────────────┘</span></span></code></pre></div><h3 id="slots-的限制"><strong>slots</strong> 的限制</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="k">class</span> <span class="nc">Base</span><span class="p">:</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">    <span class="vm">__slots__</span> <span class="o">=</span> <span class="p">[</span><span class="s1">&#39;x&#39;</span><span class="p">]</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="k">class</span> <span class="nc">Derived</span><span class="p">(</span><span class="n">Base</span><span class="p">):</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">    <span class="vm">__slots__</span> <span class="o">=</span> <span class="p">[</span><span class="s1">&#39;y&#39;</span><span class="p">]</span>  <span class="c1"># 不能與父類別重複</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">    <span class="k">def</span> <span class="fm">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">x</span> <span class="o">=</span> <span class="mi">1</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">y</span> <span class="o">=</span> <span class="mi">2</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">        <span class="c1"># self.z = 3  # 錯誤！沒有 __dict__</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">
</span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="c1"># 如果需要動態屬性，加入 &#39;__dict__&#39;</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="k">class</span> <span class="nc">Flexible</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">    <span class="vm">__slots__</span> <span class="o">=</span> <span class="p">[</span><span class="s1">&#39;x&#39;</span><span class="p">,</span> <span class="s1">&#39;__dict__&#39;</span><span class="p">]</span></span></span></code></pre></div><h3 id="使用弱參考">使用弱參考</h3>
<p>弱參考不增加參考計數，適合用於快取：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="kn">import</span> <span class="nn">weakref</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="k">class</span> <span class="nc">ExpensiveObject</span><span class="p">:</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">    <span class="k">def</span> <span class="fm">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">value</span><span class="p">):</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">value</span> <span class="o">=</span> <span class="n">value</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="c1"># 建立物件和弱參考</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="n">obj</span> <span class="o">=</span> <span class="n">ExpensiveObject</span><span class="p">(</span><span class="mi">42</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="n">weak_ref</span> <span class="o">=</span> <span class="n">weakref</span><span class="o">.</span><span class="n">ref</span><span class="p">(</span><span class="n">obj</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="nb">print</span><span class="p">(</span><span class="n">weak_ref</span><span class="p">())</span>  <span class="c1"># &lt;ExpensiveObject object&gt;</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="nb">print</span><span class="p">(</span><span class="n">weak_ref</span><span class="p">()</span><span class="o">.</span><span class="n">value</span><span class="p">)</span>  <span class="c1"># 42</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">
</span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="c1"># 刪除強參考</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="k">del</span> <span class="n">obj</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">
</span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="nb">print</span><span class="p">(</span><span class="n">weak_ref</span><span class="p">())</span>  <span class="c1"># None（物件已被回收）</span></span></span></code></pre></div><h4 id="使用-weakvaluedictionary-實作快取">使用 WeakValueDictionary 實作快取</h4>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="kn">import</span> <span class="nn">weakref</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="k">class</span> <span class="nc">Cache</span><span class="p">:</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">    <span class="k">def</span> <span class="fm">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">_cache</span> <span class="o">=</span> <span class="n">weakref</span><span class="o">.</span><span class="n">WeakValueDictionary</span><span class="p">()</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">    <span class="k">def</span> <span class="nf">get</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">key</span><span class="p">,</span> <span class="n">factory</span><span class="p">):</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">        <span class="n">value</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_cache</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="n">key</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">        <span class="k">if</span> <span class="n">value</span> <span class="ow">is</span> <span class="kc">None</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">            <span class="n">value</span> <span class="o">=</span> <span class="n">factory</span><span class="p">()</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">            <span class="bp">self</span><span class="o">.</span><span class="n">_cache</span><span class="p">[</span><span class="n">key</span><span class="p">]</span> <span class="o">=</span> <span class="n">value</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">        <span class="k">return</span> <span class="n">value</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">
</span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="n">cache</span> <span class="o">=</span> <span class="n">Cache</span><span class="p">()</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">
</span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="k">def</span> <span class="nf">create_expensive</span><span class="p">():</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl">    <span class="k">return</span> <span class="n">ExpensiveObject</span><span class="p">(</span><span class="mi">100</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl">
</span></span><span class="line"><span class="ln">19</span><span class="cl"><span class="n">obj</span> <span class="o">=</span> <span class="n">cache</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s1">&#39;key1&#39;</span><span class="p">,</span> <span class="n">create_expensive</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">20</span><span class="cl"><span class="c1"># 當 obj 不再被使用時，快取會自動清理</span></span></span></code></pre></div><hr>
<h2 id="實作層記憶體分析工具">【實作層】記憶體分析工具</h2>
<h3 id="使用-tracemalloc">使用 tracemalloc</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="kn">import</span> <span class="nn">tracemalloc</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="c1"># 開始追蹤</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="n">tracemalloc</span><span class="o">.</span><span class="n">start</span><span class="p">()</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="c1"># 執行程式碼</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="n">data</span> <span class="o">=</span> <span class="p">[</span><span class="n">i</span> <span class="o">**</span> <span class="mi">2</span> <span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="mi">10000</span><span class="p">)]</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="n">more_data</span> <span class="o">=</span> <span class="p">{</span><span class="nb">str</span><span class="p">(</span><span class="n">i</span><span class="p">):</span> <span class="n">i</span> <span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="mi">10000</span><span class="p">)}</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="c1"># 取得記憶體快照</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="n">snapshot</span> <span class="o">=</span> <span class="n">tracemalloc</span><span class="o">.</span><span class="n">take_snapshot</span><span class="p">()</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">
</span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="c1"># 顯示前 10 個記憶體使用最多的位置</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="n">top_stats</span> <span class="o">=</span> <span class="n">snapshot</span><span class="o">.</span><span class="n">statistics</span><span class="p">(</span><span class="s1">&#39;lineno&#39;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="k">for</span> <span class="n">stat</span> <span class="ow">in</span> <span class="n">top_stats</span><span class="p">[:</span><span class="mi">10</span><span class="p">]:</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">    <span class="nb">print</span><span class="p">(</span><span class="n">stat</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl">
</span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="c1"># 比較兩個快照</span>
</span></span><span class="line"><span class="ln">19</span><span class="cl"><span class="n">tracemalloc</span><span class="o">.</span><span class="n">start</span><span class="p">()</span>
</span></span><span class="line"><span class="ln">20</span><span class="cl"><span class="n">snapshot1</span> <span class="o">=</span> <span class="n">tracemalloc</span><span class="o">.</span><span class="n">take_snapshot</span><span class="p">()</span>
</span></span><span class="line"><span class="ln">21</span><span class="cl">
</span></span><span class="line"><span class="ln">22</span><span class="cl"><span class="c1"># 執行更多程式碼</span>
</span></span><span class="line"><span class="ln">23</span><span class="cl"><span class="n">big_list</span> <span class="o">=</span> <span class="nb">list</span><span class="p">(</span><span class="nb">range</span><span class="p">(</span><span class="mi">100000</span><span class="p">))</span>
</span></span><span class="line"><span class="ln">24</span><span class="cl">
</span></span><span class="line"><span class="ln">25</span><span class="cl"><span class="n">snapshot2</span> <span class="o">=</span> <span class="n">tracemalloc</span><span class="o">.</span><span class="n">take_snapshot</span><span class="p">()</span>
</span></span><span class="line"><span class="ln">26</span><span class="cl"><span class="n">diff</span> <span class="o">=</span> <span class="n">snapshot2</span><span class="o">.</span><span class="n">compare_to</span><span class="p">(</span><span class="n">snapshot1</span><span class="p">,</span> <span class="s1">&#39;lineno&#39;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">27</span><span class="cl">
</span></span><span class="line"><span class="ln">28</span><span class="cl"><span class="k">for</span> <span class="n">stat</span> <span class="ow">in</span> <span class="n">diff</span><span class="p">[:</span><span class="mi">5</span><span class="p">]:</span>
</span></span><span class="line"><span class="ln">29</span><span class="cl">    <span class="nb">print</span><span class="p">(</span><span class="n">stat</span><span class="p">)</span></span></span></code></pre></div><h3 id="使用-gc-模組除錯">使用 gc 模組除錯</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="kn">import</span> <span class="nn">gc</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="c1"># 啟用除錯</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="n">gc</span><span class="o">.</span><span class="n">set_debug</span><span class="p">(</span><span class="n">gc</span><span class="o">.</span><span class="n">DEBUG_LEAK</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="c1"># 找出無法回收的物件</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="n">gc</span><span class="o">.</span><span class="n">collect</span><span class="p">()</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="nb">print</span><span class="p">(</span><span class="n">gc</span><span class="o">.</span><span class="n">garbage</span><span class="p">)</span>  <span class="c1"># 無法回收的物件列表</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="c1"># 取得所有被追蹤的物件</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="n">all_objects</span> <span class="o">=</span> <span class="n">gc</span><span class="o">.</span><span class="n">get_objects</span><span class="p">()</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="nb">print</span><span class="p">(</span><span class="sa">f</span><span class="s2">&#34;被追蹤的物件數量: </span><span class="si">{</span><span class="nb">len</span><span class="p">(</span><span class="n">all_objects</span><span class="p">)</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">
</span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="c1"># 找出特定類別的實例</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="k">class</span> <span class="nc">MyClass</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">    <span class="k">pass</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl">
</span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="n">instances</span> <span class="o">=</span> <span class="p">[</span><span class="n">obj</span> <span class="k">for</span> <span class="n">obj</span> <span class="ow">in</span> <span class="n">gc</span><span class="o">.</span><span class="n">get_objects</span><span class="p">()</span> <span class="k">if</span> <span class="nb">isinstance</span><span class="p">(</span><span class="n">obj</span><span class="p">,</span> <span class="n">MyClass</span><span class="p">)]</span>
</span></span><span class="line"><span class="ln">19</span><span class="cl"><span class="nb">print</span><span class="p">(</span><span class="sa">f</span><span class="s2">&#34;MyClass 實例數量: </span><span class="si">{</span><span class="nb">len</span><span class="p">(</span><span class="n">instances</span><span class="p">)</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">)</span></span></span></code></pre></div><h3 id="檢測記憶體洩漏">檢測記憶體洩漏</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="kn">import</span> <span class="nn">gc</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="kn">import</span> <span class="nn">tracemalloc</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="k">def</span> <span class="nf">find_memory_leak</span><span class="p">():</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">    <span class="n">tracemalloc</span><span class="o">.</span><span class="n">start</span><span class="p">()</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">    <span class="c1"># 記錄初始狀態</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">    <span class="n">gc</span><span class="o">.</span><span class="n">collect</span><span class="p">()</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">    <span class="n">snapshot1</span> <span class="o">=</span> <span class="n">tracemalloc</span><span class="o">.</span><span class="n">take_snapshot</span><span class="p">()</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">
</span></span><span class="line"><span class="ln">11</span><span class="cl">    <span class="c1"># 執行可能洩漏的程式碼</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">    <span class="k">for</span> <span class="n">_</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="mi">1000</span><span class="p">):</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">        <span class="n">suspicious_function</span><span class="p">()</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">
</span></span><span class="line"><span class="ln">15</span><span class="cl">    <span class="c1"># 記錄最終狀態</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">    <span class="n">gc</span><span class="o">.</span><span class="n">collect</span><span class="p">()</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl">    <span class="n">snapshot2</span> <span class="o">=</span> <span class="n">tracemalloc</span><span class="o">.</span><span class="n">take_snapshot</span><span class="p">()</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl">
</span></span><span class="line"><span class="ln">19</span><span class="cl">    <span class="c1"># 比較</span>
</span></span><span class="line"><span class="ln">20</span><span class="cl">    <span class="n">diff</span> <span class="o">=</span> <span class="n">snapshot2</span><span class="o">.</span><span class="n">compare_to</span><span class="p">(</span><span class="n">snapshot1</span><span class="p">,</span> <span class="s1">&#39;lineno&#39;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">21</span><span class="cl">
</span></span><span class="line"><span class="ln">22</span><span class="cl">    <span class="nb">print</span><span class="p">(</span><span class="s2">&#34;記憶體增長最多的位置：&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">23</span><span class="cl">    <span class="k">for</span> <span class="n">stat</span> <span class="ow">in</span> <span class="n">diff</span><span class="p">[:</span><span class="mi">10</span><span class="p">]:</span>
</span></span><span class="line"><span class="ln">24</span><span class="cl">        <span class="nb">print</span><span class="p">(</span><span class="n">stat</span><span class="p">)</span></span></span></code></pre></div><hr>
<h2 id="實戰常見記憶體問題">【實戰】常見記憶體問題</h2>
<h3 id="問題-1大量小物件">問題 1：大量小物件</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1"># 不好：建立大量小物件</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="k">class</span> <span class="nc">Point</span><span class="p">:</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">    <span class="k">def</span> <span class="fm">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">x</span><span class="p">,</span> <span class="n">y</span><span class="p">):</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">x</span> <span class="o">=</span> <span class="n">x</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">y</span> <span class="o">=</span> <span class="n">y</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="n">points</span> <span class="o">=</span> <span class="p">[</span><span class="n">Point</span><span class="p">(</span><span class="n">i</span><span class="p">,</span> <span class="n">i</span><span class="p">)</span> <span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="mi">1000000</span><span class="p">)]</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="c1"># 好：使用 __slots__</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="k">class</span> <span class="nc">Point</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">    <span class="vm">__slots__</span> <span class="o">=</span> <span class="p">[</span><span class="s1">&#39;x&#39;</span><span class="p">,</span> <span class="s1">&#39;y&#39;</span><span class="p">]</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">    <span class="k">def</span> <span class="fm">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">x</span><span class="p">,</span> <span class="n">y</span><span class="p">):</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">x</span> <span class="o">=</span> <span class="n">x</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">y</span> <span class="o">=</span> <span class="n">y</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">
</span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="c1"># 更好：使用 NumPy（如果是數值資料）</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="kn">import</span> <span class="nn">numpy</span> <span class="k">as</span> <span class="nn">np</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="n">points</span> <span class="o">=</span> <span class="n">np</span><span class="o">.</span><span class="n">zeros</span><span class="p">((</span><span class="mi">1000000</span><span class="p">,</span> <span class="mi">2</span><span class="p">))</span></span></span></code></pre></div><h3 id="問題-2循環參考">問題 2：循環參考</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1"># 不好：物件間的循環參考</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="k">class</span> <span class="nc">Parent</span><span class="p">:</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">    <span class="k">def</span> <span class="fm">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">children</span> <span class="o">=</span> <span class="p">[]</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="k">class</span> <span class="nc">Child</span><span class="p">:</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">    <span class="k">def</span> <span class="fm">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">parent</span><span class="p">):</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">parent</span> <span class="o">=</span> <span class="n">parent</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">        <span class="n">parent</span><span class="o">.</span><span class="n">children</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="bp">self</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="c1"># 好：使用弱參考</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="kn">import</span> <span class="nn">weakref</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">
</span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="k">class</span> <span class="nc">Child</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">    <span class="k">def</span> <span class="fm">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">parent</span><span class="p">):</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">parent</span> <span class="o">=</span> <span class="n">weakref</span><span class="o">.</span><span class="n">ref</span><span class="p">(</span><span class="n">parent</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl">        <span class="n">parent</span><span class="o">.</span><span class="n">children</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="bp">self</span><span class="p">)</span></span></span></code></pre></div><h3 id="問題-3全域變數累積">問題 3：全域變數累積</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1"># 不好：全域快取無限增長</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="n">_cache</span> <span class="o">=</span> <span class="p">{}</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="k">def</span> <span class="nf">process</span><span class="p">(</span><span class="n">key</span><span class="p">,</span> <span class="n">value</span><span class="p">):</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">    <span class="k">if</span> <span class="n">key</span> <span class="ow">not</span> <span class="ow">in</span> <span class="n">_cache</span><span class="p">:</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">        <span class="n">_cache</span><span class="p">[</span><span class="n">key</span><span class="p">]</span> <span class="o">=</span> <span class="n">expensive_compute</span><span class="p">(</span><span class="n">value</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">    <span class="k">return</span> <span class="n">_cache</span><span class="p">[</span><span class="n">key</span><span class="p">]</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="c1"># 好：使用 LRU cache</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="kn">from</span> <span class="nn">functools</span> <span class="kn">import</span> <span class="n">lru_cache</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">
</span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="nd">@lru_cache</span><span class="p">(</span><span class="n">maxsize</span><span class="o">=</span><span class="mi">1000</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="k">def</span> <span class="nf">process</span><span class="p">(</span><span class="n">key</span><span class="p">,</span> <span class="n">value</span><span class="p">):</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">    <span class="k">return</span> <span class="n">expensive_compute</span><span class="p">(</span><span class="n">value</span><span class="p">)</span></span></span></code></pre></div><hr>
<h2 id="思考題">思考題</h2>
<ol>
<li>為什麼 Python 需要同時使用參考計數和垃圾回收？只用其中一種不行嗎？</li>
<li><code>__slots__</code> 為什麼不能用於繼承自內建型別的類別？</li>
<li>在什麼情況下應該手動呼叫 <code>gc.collect()</code>？</li>
</ol>
<h2 id="實作練習">實作練習</h2>
<ol>
<li>使用 <code>tracemalloc</code> 分析一個現有程式的記憶體使用</li>
<li>將一個使用大量物件的程式改用 <code>__slots__</code> 優化</li>
<li>使用 <code>WeakValueDictionary</code> 實作一個自動清理的快取</li>
</ol>
<h2 id="延伸閱讀">延伸閱讀</h2>
<ul>
<li><a href="https://docs.python.org/3/library/gc.html">Python 官方 - gc 模組</a></li>
<li><a href="https://docs.python.org/3/library/tracemalloc.html">Python 官方 - tracemalloc 模組</a></li>
<li><a href="https://blog.codingconfessions.com/p/cpython-garbage-collection-internals">Coding Confessions - CPython GC Internals</a></li>
</ul>
<hr>
<p><em>上一章：<a href="/blog/python-advanced/04-cpython-internals/object-model/" data-link-title="3.1 PyObject 與物件模型" data-link-desc="深入理解 Python 的物件模型">PyObject 與物件模型</a></em>
<em>下一章：<a href="/blog/python-advanced/04-cpython-internals/bytecode/" data-link-title="3.3 Bytecode 與虛擬機" data-link-desc="理解 Python 的執行過程">Bytecode 與虛擬機</a></em></p>
]]></content:encoded></item><item><title>1.4 共享狀態與複製邊界</title><link>https://tarrragon.github.io/blog/go-advanced/01-concurrency-patterns/shared-state/</link><pubDate>Wed, 22 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/go-advanced/01-concurrency-patterns/shared-state/</guid><description>&lt;p>共享狀態的核心規則是同一份可變資料若會被多個 goroutine 存取，就必須有明確 owner 與保護邊界。Map 需要同步，slice 回傳前通常要 copy，可變指標不能隨意暴露，修改行為應集中在擁有狀態的型別內。&lt;/p>
&lt;h2 id="本章目標">本章目標&lt;/h2>
&lt;p>學完本章後，你將能夠：&lt;/p>
&lt;ol>
&lt;li>判斷哪個型別擁有共享狀態&lt;/li>
&lt;li>用 &lt;code>sync.RWMutex&lt;/code> 保護 map 與 slice&lt;/li>
&lt;li>避免回傳內部 map、slice、pointer&lt;/li>
&lt;li>分辨 shallow copy 與 deep copy 的邊界&lt;/li>
&lt;li>用測試與 race detector 驗證共享狀態安全性&lt;/li>
&lt;/ol>
&lt;hr>
&lt;h2 id="觀察共享狀態風險通常延遲出現">【觀察】共享狀態風險通常延遲出現&lt;/h2>
&lt;p>共享狀態的核心風險是錯誤可能只在特定併發時序下出現。單元測試可能通過，本地手動操作也正常，但高流量下會出現 data race、map panic、狀態被外部修改或歷史資料被覆蓋。&lt;/p>
&lt;p>反模式示意：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-go" data-lang="go">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="kd">type&lt;/span> &lt;span class="nx">Store&lt;/span> &lt;span class="kd">struct&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl"> &lt;span class="nx">users&lt;/span> &lt;span class="kd">map&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="kt">string&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="nx">User&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl">&lt;span class="kd">func&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">s&lt;/span> &lt;span class="o">*&lt;/span>&lt;span class="nx">Store&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="nf">Save&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">user&lt;/span> &lt;span class="nx">User&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl"> &lt;span class="nx">s&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">users&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="nx">user&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">ID&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="nx">user&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">&lt;span class="kd">func&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">s&lt;/span> &lt;span class="o">*&lt;/span>&lt;span class="nx">Store&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="nf">Users&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="kd">map&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="kt">string&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="nx">User&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="nx">s&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">users&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>這個型別有兩個問題：map 沒有同步保護，且 &lt;code>Users&lt;/code> 直接暴露內部 map。呼叫端拿到 map 後可以繞過 &lt;code>Store&lt;/code> 修改資料。&lt;/p>
&lt;h2 id="判讀mutex-保護的是狀態不變式">【判讀】mutex 保護的是狀態不變式&lt;/h2>
&lt;p>Mutex 的核心責任不是讓程式「不會同時跑」，而是保護某一組資料的不變式。只要讀寫同一份可變資料，就應該由同一個 owner 控制 lock。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-go" data-lang="go">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="kd">type&lt;/span> &lt;span class="nx">UserRepository&lt;/span> &lt;span class="kd">struct&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl"> &lt;span class="nx">mu&lt;/span> &lt;span class="nx">sync&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">RWMutex&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl"> &lt;span class="nx">users&lt;/span> &lt;span class="kd">map&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="kt">string&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="nx">User&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">&lt;span class="kd">func&lt;/span> &lt;span class="nf">NewUserRepository&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="o">*&lt;/span>&lt;span class="nx">UserRepository&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="o">&amp;amp;&lt;/span>&lt;span class="nx">UserRepository&lt;/span>&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl"> &lt;span class="nx">users&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="nb">make&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="kd">map&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="kt">string&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="nx">User&lt;/span>&lt;span class="p">),&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>UserRepository&lt;/code> 是 &lt;code>users&lt;/code> map 的 owner。外部程式不應持有 &lt;code>users&lt;/code> 的 reference，也不應知道它用 map、資料庫或其他結構保存。&lt;/p>
&lt;h2 id="執行所有讀寫都經過-owner-method">【執行】所有讀寫都經過 owner method&lt;/h2>
&lt;p>共享 map 的核心規則是所有讀寫都經過同一組方法。寫入使用 &lt;code>Lock&lt;/code>，讀取使用 &lt;code>RLock&lt;/code>。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-go" data-lang="go">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="kd">func&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">r&lt;/span> &lt;span class="o">*&lt;/span>&lt;span class="nx">UserRepository&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="nf">Save&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">ctx&lt;/span> &lt;span class="nx">context&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">Context&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">user&lt;/span> &lt;span class="nx">User&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="kt">error&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl"> &lt;span class="nx">r&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">mu&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Lock&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl"> &lt;span class="k">defer&lt;/span> &lt;span class="nx">r&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">mu&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Unlock&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl"> &lt;span class="nx">r&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">users&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="nx">user&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">ID&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="nx">user&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="kc">nil&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">&lt;span class="kd">func&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">r&lt;/span> &lt;span class="o">*&lt;/span>&lt;span class="nx">UserRepository&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="nf">Find&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">ctx&lt;/span> &lt;span class="nx">context&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">Context&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">id&lt;/span> &lt;span class="kt">string&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">User&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="kt">bool&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="kt">error&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl"> &lt;span class="nx">r&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">mu&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">RLock&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl"> &lt;span class="k">defer&lt;/span> &lt;span class="nx">r&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">mu&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">RUnlock&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">13&lt;/span>&lt;span class="cl"> &lt;span class="nx">user&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">ok&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="nx">r&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">users&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="nx">id&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">14&lt;/span>&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="nx">user&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">ok&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="kc">nil&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">15&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>context.Context&lt;/code> 在 memory repository 裡可能用不到，但保留在 method signature 可以讓未來改成資料庫或遠端儲存時支援取消。這是 repository port 常見的演進邊界。&lt;/p>
&lt;h2 id="判讀回傳內部-map-會破壞-lock">【判讀】回傳內部 map 會破壞 lock&lt;/h2>
&lt;p>回傳 map 的核心風險是鎖只保護到方法結束。方法回傳後，呼叫端拿到的仍然是同一份 map，任何修改都會繞過 owner。&lt;/p>
&lt;p>反模式：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-go" data-lang="go">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="kd">func&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">r&lt;/span> &lt;span class="o">*&lt;/span>&lt;span class="nx">UserRepository&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="nf">UnsafeUsers&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">ctx&lt;/span> &lt;span class="nx">context&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">Context&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="kd">map&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="kt">string&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="nx">User&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl"> &lt;span class="nx">r&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">mu&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">RLock&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl"> &lt;span class="k">defer&lt;/span> &lt;span class="nx">r&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">mu&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">RUnlock&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="nx">r&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">users&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>這段程式看起來有加鎖，但鎖釋放後外部仍能修改內部 map：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-go" data-lang="go">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="nx">users&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="nx">repo&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">UnsafeUsers&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">ctx&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="nb">delete&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">users&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s">&amp;#34;user_1&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>安全做法是回傳 copy：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-go" data-lang="go">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="kd">func&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">r&lt;/span> &lt;span class="o">*&lt;/span>&lt;span class="nx">UserRepository&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="nf">Users&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">ctx&lt;/span> &lt;span class="nx">context&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">Context&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="kd">map&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="kt">string&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="nx">User&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="kt">error&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl"> &lt;span class="nx">r&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">mu&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">RLock&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl"> &lt;span class="k">defer&lt;/span> &lt;span class="nx">r&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">mu&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">RUnlock&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl"> &lt;span class="nx">result&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="nb">make&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="kd">map&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="kt">string&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="nx">User&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nb">len&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">r&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">users&lt;/span>&lt;span class="p">))&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl"> &lt;span class="k">for&lt;/span> &lt;span class="nx">id&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">user&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="k">range&lt;/span> &lt;span class="nx">r&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">users&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl"> &lt;span class="nx">result&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="nx">id&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="nx">user&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="nx">result&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="kc">nil&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>呼叫端可以自由修改 &lt;code>result&lt;/code>，不會影響 repository 內部狀態。&lt;/p>
&lt;h2 id="判讀slice-copy-保護的是底層-array">【判讀】slice copy 保護的是底層 array&lt;/h2>
&lt;p>Slice 的核心風險是 slice header 會被複製，但底層 array 可能共享。直接回傳 slice 會讓呼叫端修改 owner 的內部資料。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-go" data-lang="go">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="kd">type&lt;/span> &lt;span class="nx">RecentEvents&lt;/span> &lt;span class="kd">struct&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl"> &lt;span class="nx">mu&lt;/span> &lt;span class="nx">sync&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">RWMutex&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl"> &lt;span class="nx">events&lt;/span> &lt;span class="p">[]&lt;/span>&lt;span class="nx">Event&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">&lt;span class="kd">func&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">r&lt;/span> &lt;span class="o">*&lt;/span>&lt;span class="nx">RecentEvents&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="nf">Append&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">ctx&lt;/span> &lt;span class="nx">context&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">Context&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">event&lt;/span> &lt;span class="nx">Event&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl"> &lt;span class="nx">r&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">mu&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Lock&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl"> &lt;span class="k">defer&lt;/span> &lt;span class="nx">r&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">mu&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Unlock&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl"> &lt;span class="nx">r&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">events&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="nb">append&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">r&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">events&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">event&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>安全的 list method：&lt;/p></description><content:encoded><![CDATA[<p>共享狀態的核心規則是同一份可變資料若會被多個 goroutine 存取，就必須有明確 owner 與保護邊界。Map 需要同步，slice 回傳前通常要 copy，可變指標不能隨意暴露，修改行為應集中在擁有狀態的型別內。</p>
<h2 id="本章目標">本章目標</h2>
<p>學完本章後，你將能夠：</p>
<ol>
<li>判斷哪個型別擁有共享狀態</li>
<li>用 <code>sync.RWMutex</code> 保護 map 與 slice</li>
<li>避免回傳內部 map、slice、pointer</li>
<li>分辨 shallow copy 與 deep copy 的邊界</li>
<li>用測試與 race detector 驗證共享狀態安全性</li>
</ol>
<hr>
<h2 id="觀察共享狀態風險通常延遲出現">【觀察】共享狀態風險通常延遲出現</h2>
<p>共享狀態的核心風險是錯誤可能只在特定併發時序下出現。單元測試可能通過，本地手動操作也正常，但高流量下會出現 data race、map panic、狀態被外部修改或歷史資料被覆蓋。</p>
<p>反模式示意：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="kd">type</span> <span class="nx">Store</span> <span class="kd">struct</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">    <span class="nx">users</span> <span class="kd">map</span><span class="p">[</span><span class="kt">string</span><span class="p">]</span><span class="nx">User</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">
</span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">s</span> <span class="o">*</span><span class="nx">Store</span><span class="p">)</span> <span class="nf">Save</span><span class="p">(</span><span class="nx">user</span> <span class="nx">User</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">    <span class="nx">s</span><span class="p">.</span><span class="nx">users</span><span class="p">[</span><span class="nx">user</span><span class="p">.</span><span class="nx">ID</span><span class="p">]</span> <span class="p">=</span> <span class="nx">user</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">s</span> <span class="o">*</span><span class="nx">Store</span><span class="p">)</span> <span class="nf">Users</span><span class="p">()</span> <span class="kd">map</span><span class="p">[</span><span class="kt">string</span><span class="p">]</span><span class="nx">User</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">    <span class="k">return</span> <span class="nx">s</span><span class="p">.</span><span class="nx">users</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>這個型別有兩個問題：map 沒有同步保護，且 <code>Users</code> 直接暴露內部 map。呼叫端拿到 map 後可以繞過 <code>Store</code> 修改資料。</p>
<h2 id="判讀mutex-保護的是狀態不變式">【判讀】mutex 保護的是狀態不變式</h2>
<p>Mutex 的核心責任不是讓程式「不會同時跑」，而是保護某一組資料的不變式。只要讀寫同一份可變資料，就應該由同一個 owner 控制 lock。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="kd">type</span> <span class="nx">UserRepository</span> <span class="kd">struct</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">    <span class="nx">mu</span>    <span class="nx">sync</span><span class="p">.</span><span class="nx">RWMutex</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">    <span class="nx">users</span> <span class="kd">map</span><span class="p">[</span><span class="kt">string</span><span class="p">]</span><span class="nx">User</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="kd">func</span> <span class="nf">NewUserRepository</span><span class="p">()</span> <span class="o">*</span><span class="nx">UserRepository</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">    <span class="k">return</span> <span class="o">&amp;</span><span class="nx">UserRepository</span><span class="p">{</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">        <span class="nx">users</span><span class="p">:</span> <span class="nb">make</span><span class="p">(</span><span class="kd">map</span><span class="p">[</span><span class="kt">string</span><span class="p">]</span><span class="nx">User</span><span class="p">),</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p><code>UserRepository</code> 是 <code>users</code> map 的 owner。外部程式不應持有 <code>users</code> 的 reference，也不應知道它用 map、資料庫或其他結構保存。</p>
<h2 id="執行所有讀寫都經過-owner-method">【執行】所有讀寫都經過 owner method</h2>
<p>共享 map 的核心規則是所有讀寫都經過同一組方法。寫入使用 <code>Lock</code>，讀取使用 <code>RLock</code>。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">r</span> <span class="o">*</span><span class="nx">UserRepository</span><span class="p">)</span> <span class="nf">Save</span><span class="p">(</span><span class="nx">ctx</span> <span class="nx">context</span><span class="p">.</span><span class="nx">Context</span><span class="p">,</span> <span class="nx">user</span> <span class="nx">User</span><span class="p">)</span> <span class="kt">error</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">    <span class="nx">r</span><span class="p">.</span><span class="nx">mu</span><span class="p">.</span><span class="nf">Lock</span><span class="p">()</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">    <span class="k">defer</span> <span class="nx">r</span><span class="p">.</span><span class="nx">mu</span><span class="p">.</span><span class="nf">Unlock</span><span class="p">()</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">    <span class="nx">r</span><span class="p">.</span><span class="nx">users</span><span class="p">[</span><span class="nx">user</span><span class="p">.</span><span class="nx">ID</span><span class="p">]</span> <span class="p">=</span> <span class="nx">user</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">    <span class="k">return</span> <span class="kc">nil</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">r</span> <span class="o">*</span><span class="nx">UserRepository</span><span class="p">)</span> <span class="nf">Find</span><span class="p">(</span><span class="nx">ctx</span> <span class="nx">context</span><span class="p">.</span><span class="nx">Context</span><span class="p">,</span> <span class="nx">id</span> <span class="kt">string</span><span class="p">)</span> <span class="p">(</span><span class="nx">User</span><span class="p">,</span> <span class="kt">bool</span><span class="p">,</span> <span class="kt">error</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">    <span class="nx">r</span><span class="p">.</span><span class="nx">mu</span><span class="p">.</span><span class="nf">RLock</span><span class="p">()</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">    <span class="k">defer</span> <span class="nx">r</span><span class="p">.</span><span class="nx">mu</span><span class="p">.</span><span class="nf">RUnlock</span><span class="p">()</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">
</span></span><span class="line"><span class="ln">13</span><span class="cl">    <span class="nx">user</span><span class="p">,</span> <span class="nx">ok</span> <span class="o">:=</span> <span class="nx">r</span><span class="p">.</span><span class="nx">users</span><span class="p">[</span><span class="nx">id</span><span class="p">]</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">    <span class="k">return</span> <span class="nx">user</span><span class="p">,</span> <span class="nx">ok</span><span class="p">,</span> <span class="kc">nil</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p><code>context.Context</code> 在 memory repository 裡可能用不到，但保留在 method signature 可以讓未來改成資料庫或遠端儲存時支援取消。這是 repository port 常見的演進邊界。</p>
<h2 id="判讀回傳內部-map-會破壞-lock">【判讀】回傳內部 map 會破壞 lock</h2>
<p>回傳 map 的核心風險是鎖只保護到方法結束。方法回傳後，呼叫端拿到的仍然是同一份 map，任何修改都會繞過 owner。</p>
<p>反模式：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="ln">1</span><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">r</span> <span class="o">*</span><span class="nx">UserRepository</span><span class="p">)</span> <span class="nf">UnsafeUsers</span><span class="p">(</span><span class="nx">ctx</span> <span class="nx">context</span><span class="p">.</span><span class="nx">Context</span><span class="p">)</span> <span class="kd">map</span><span class="p">[</span><span class="kt">string</span><span class="p">]</span><span class="nx">User</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">    <span class="nx">r</span><span class="p">.</span><span class="nx">mu</span><span class="p">.</span><span class="nf">RLock</span><span class="p">()</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">    <span class="k">defer</span> <span class="nx">r</span><span class="p">.</span><span class="nx">mu</span><span class="p">.</span><span class="nf">RUnlock</span><span class="p">()</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">
</span></span><span class="line"><span class="ln">5</span><span class="cl">    <span class="k">return</span> <span class="nx">r</span><span class="p">.</span><span class="nx">users</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>這段程式看起來有加鎖，但鎖釋放後外部仍能修改內部 map：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="ln">1</span><span class="cl"><span class="nx">users</span> <span class="o">:=</span> <span class="nx">repo</span><span class="p">.</span><span class="nf">UnsafeUsers</span><span class="p">(</span><span class="nx">ctx</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="nb">delete</span><span class="p">(</span><span class="nx">users</span><span class="p">,</span> <span class="s">&#34;user_1&#34;</span><span class="p">)</span></span></span></code></pre></div><p>安全做法是回傳 copy：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">r</span> <span class="o">*</span><span class="nx">UserRepository</span><span class="p">)</span> <span class="nf">Users</span><span class="p">(</span><span class="nx">ctx</span> <span class="nx">context</span><span class="p">.</span><span class="nx">Context</span><span class="p">)</span> <span class="p">(</span><span class="kd">map</span><span class="p">[</span><span class="kt">string</span><span class="p">]</span><span class="nx">User</span><span class="p">,</span> <span class="kt">error</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">    <span class="nx">r</span><span class="p">.</span><span class="nx">mu</span><span class="p">.</span><span class="nf">RLock</span><span class="p">()</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">    <span class="k">defer</span> <span class="nx">r</span><span class="p">.</span><span class="nx">mu</span><span class="p">.</span><span class="nf">RUnlock</span><span class="p">()</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">    <span class="nx">result</span> <span class="o">:=</span> <span class="nb">make</span><span class="p">(</span><span class="kd">map</span><span class="p">[</span><span class="kt">string</span><span class="p">]</span><span class="nx">User</span><span class="p">,</span> <span class="nb">len</span><span class="p">(</span><span class="nx">r</span><span class="p">.</span><span class="nx">users</span><span class="p">))</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">    <span class="k">for</span> <span class="nx">id</span><span class="p">,</span> <span class="nx">user</span> <span class="o">:=</span> <span class="k">range</span> <span class="nx">r</span><span class="p">.</span><span class="nx">users</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">        <span class="nx">result</span><span class="p">[</span><span class="nx">id</span><span class="p">]</span> <span class="p">=</span> <span class="nx">user</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">    <span class="k">return</span> <span class="nx">result</span><span class="p">,</span> <span class="kc">nil</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>呼叫端可以自由修改 <code>result</code>，不會影響 repository 內部狀態。</p>
<h2 id="判讀slice-copy-保護的是底層-array">【判讀】slice copy 保護的是底層 array</h2>
<p>Slice 的核心風險是 slice header 會被複製，但底層 array 可能共享。直接回傳 slice 會讓呼叫端修改 owner 的內部資料。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="kd">type</span> <span class="nx">RecentEvents</span> <span class="kd">struct</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">    <span class="nx">mu</span>     <span class="nx">sync</span><span class="p">.</span><span class="nx">RWMutex</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">    <span class="nx">events</span> <span class="p">[]</span><span class="nx">Event</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">r</span> <span class="o">*</span><span class="nx">RecentEvents</span><span class="p">)</span> <span class="nf">Append</span><span class="p">(</span><span class="nx">ctx</span> <span class="nx">context</span><span class="p">.</span><span class="nx">Context</span><span class="p">,</span> <span class="nx">event</span> <span class="nx">Event</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">    <span class="nx">r</span><span class="p">.</span><span class="nx">mu</span><span class="p">.</span><span class="nf">Lock</span><span class="p">()</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">    <span class="k">defer</span> <span class="nx">r</span><span class="p">.</span><span class="nx">mu</span><span class="p">.</span><span class="nf">Unlock</span><span class="p">()</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">
</span></span><span class="line"><span class="ln">10</span><span class="cl">    <span class="nx">r</span><span class="p">.</span><span class="nx">events</span> <span class="p">=</span> <span class="nb">append</span><span class="p">(</span><span class="nx">r</span><span class="p">.</span><span class="nx">events</span><span class="p">,</span> <span class="nx">event</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>安全的 list method：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="ln">1</span><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">r</span> <span class="o">*</span><span class="nx">RecentEvents</span><span class="p">)</span> <span class="nf">List</span><span class="p">(</span><span class="nx">ctx</span> <span class="nx">context</span><span class="p">.</span><span class="nx">Context</span><span class="p">)</span> <span class="p">[]</span><span class="nx">Event</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">    <span class="nx">r</span><span class="p">.</span><span class="nx">mu</span><span class="p">.</span><span class="nf">RLock</span><span class="p">()</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">    <span class="k">defer</span> <span class="nx">r</span><span class="p">.</span><span class="nx">mu</span><span class="p">.</span><span class="nf">RUnlock</span><span class="p">()</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">
</span></span><span class="line"><span class="ln">5</span><span class="cl">    <span class="nx">result</span> <span class="o">:=</span> <span class="nb">make</span><span class="p">([]</span><span class="nx">Event</span><span class="p">,</span> <span class="nb">len</span><span class="p">(</span><span class="nx">r</span><span class="p">.</span><span class="nx">events</span><span class="p">))</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">    <span class="nb">copy</span><span class="p">(</span><span class="nx">result</span><span class="p">,</span> <span class="nx">r</span><span class="p">.</span><span class="nx">events</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl">    <span class="k">return</span> <span class="nx">result</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p><code>copy</code> 建立新的底層 array。呼叫端對 <code>result</code> 排序、截斷、append 或修改元素，不會改到 <code>r.events</code>。</p>
<h2 id="策略值型別可以-shallow-copy可變欄位需要-deep-copy">【策略】值型別可以 shallow copy，可變欄位需要 deep copy</h2>
<p>Copy boundary 的核心判斷是資料裡是否還包含可變 reference。若 struct 只有 string、int、time.Time 這類值型別，shallow copy 通常足夠；若 struct 包含 map、slice 或 pointer，就要考慮 deep copy。</p>
<p>值型別範例：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="ln">1</span><span class="cl"><span class="kd">type</span> <span class="nx">Event</span> <span class="kd">struct</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">    <span class="nx">ID</span>        <span class="kt">string</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">    <span class="nx">Type</span>      <span class="kt">string</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">    <span class="nx">CreatedAt</span> <span class="nx">time</span><span class="p">.</span><span class="nx">Time</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>這種 <code>Event</code> 放在 slice 裡，用 <code>copy</code> 複製 slice 通常足夠。</p>
<p>含可變欄位的範例：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="ln">1</span><span class="cl"><span class="kd">type</span> <span class="nx">Event</span> <span class="kd">struct</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">    <span class="nx">ID</span>       <span class="kt">string</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">    <span class="nx">Type</span>     <span class="kt">string</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">    <span class="nx">Metadata</span> <span class="kd">map</span><span class="p">[</span><span class="kt">string</span><span class="p">]</span><span class="kt">string</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>這時只 copy slice 不夠，因為每個 <code>Event.Metadata</code> 仍然指向同一份 map。需要 clone：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="kd">func</span> <span class="nf">CloneEvent</span><span class="p">(</span><span class="nx">event</span> <span class="nx">Event</span><span class="p">)</span> <span class="nx">Event</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">    <span class="nx">cloned</span> <span class="o">:=</span> <span class="nx">event</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">    <span class="k">if</span> <span class="nx">event</span><span class="p">.</span><span class="nx">Metadata</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">        <span class="nx">cloned</span><span class="p">.</span><span class="nx">Metadata</span> <span class="p">=</span> <span class="nb">make</span><span class="p">(</span><span class="kd">map</span><span class="p">[</span><span class="kt">string</span><span class="p">]</span><span class="kt">string</span><span class="p">,</span> <span class="nb">len</span><span class="p">(</span><span class="nx">event</span><span class="p">.</span><span class="nx">Metadata</span><span class="p">))</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">        <span class="k">for</span> <span class="nx">key</span><span class="p">,</span> <span class="nx">value</span> <span class="o">:=</span> <span class="k">range</span> <span class="nx">event</span><span class="p">.</span><span class="nx">Metadata</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">            <span class="nx">cloned</span><span class="p">.</span><span class="nx">Metadata</span><span class="p">[</span><span class="nx">key</span><span class="p">]</span> <span class="p">=</span> <span class="nx">value</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">        <span class="p">}</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">    <span class="k">return</span> <span class="nx">cloned</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>是否需要 deep copy 取決於 API 承諾。如果呼叫端不應修改 repository 內部資料，就要複製所有可變 reference。</p>
<h2 id="判讀回傳-pointer-要代表明確修改權">【判讀】回傳 pointer 要代表明確修改權</h2>
<p>Pointer 回傳的核心語意是呼叫端取得同一份資料的參照。若資料屬於共享狀態，回傳 pointer 通常會破壞 owner 邊界。</p>
<p>容易誤解的 API：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">r</span> <span class="o">*</span><span class="nx">UserRepository</span><span class="p">)</span> <span class="nf">FindPointer</span><span class="p">(</span><span class="nx">ctx</span> <span class="nx">context</span><span class="p">.</span><span class="nx">Context</span><span class="p">,</span> <span class="nx">id</span> <span class="kt">string</span><span class="p">)</span> <span class="p">(</span><span class="o">*</span><span class="nx">User</span><span class="p">,</span> <span class="kt">bool</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">    <span class="nx">r</span><span class="p">.</span><span class="nx">mu</span><span class="p">.</span><span class="nf">RLock</span><span class="p">()</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">    <span class="k">defer</span> <span class="nx">r</span><span class="p">.</span><span class="nx">mu</span><span class="p">.</span><span class="nf">RUnlock</span><span class="p">()</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">    <span class="nx">user</span><span class="p">,</span> <span class="nx">ok</span> <span class="o">:=</span> <span class="nx">r</span><span class="p">.</span><span class="nx">users</span><span class="p">[</span><span class="nx">id</span><span class="p">]</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">    <span class="k">if</span> <span class="p">!</span><span class="nx">ok</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">        <span class="k">return</span> <span class="kc">nil</span><span class="p">,</span> <span class="kc">false</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">    <span class="k">return</span> <span class="o">&amp;</span><span class="nx">user</span><span class="p">,</span> <span class="kc">true</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>這段程式回傳的是區域變數 <code>user</code> 的指標，不是 map 內部資料的可修改入口。呼叫端修改這個 pointer，不會保存回 repository。API 看起來像能修改，實際不能，語意不清楚。</p>
<p>更清楚的做法是回傳 value，並提供明確 update method：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">r</span> <span class="o">*</span><span class="nx">UserRepository</span><span class="p">)</span> <span class="nf">UpdateEmail</span><span class="p">(</span><span class="nx">ctx</span> <span class="nx">context</span><span class="p">.</span><span class="nx">Context</span><span class="p">,</span> <span class="nx">id</span> <span class="kt">string</span><span class="p">,</span> <span class="nx">email</span> <span class="kt">string</span><span class="p">)</span> <span class="p">(</span><span class="kt">bool</span><span class="p">,</span> <span class="kt">error</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">    <span class="nx">r</span><span class="p">.</span><span class="nx">mu</span><span class="p">.</span><span class="nf">Lock</span><span class="p">()</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">    <span class="k">defer</span> <span class="nx">r</span><span class="p">.</span><span class="nx">mu</span><span class="p">.</span><span class="nf">Unlock</span><span class="p">()</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">    <span class="nx">user</span><span class="p">,</span> <span class="nx">ok</span> <span class="o">:=</span> <span class="nx">r</span><span class="p">.</span><span class="nx">users</span><span class="p">[</span><span class="nx">id</span><span class="p">]</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">    <span class="k">if</span> <span class="p">!</span><span class="nx">ok</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">        <span class="k">return</span> <span class="kc">false</span><span class="p">,</span> <span class="kc">nil</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">
</span></span><span class="line"><span class="ln">10</span><span class="cl">    <span class="nx">user</span><span class="p">.</span><span class="nx">Email</span> <span class="p">=</span> <span class="nx">email</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">    <span class="nx">r</span><span class="p">.</span><span class="nx">users</span><span class="p">[</span><span class="nx">id</span><span class="p">]</span> <span class="p">=</span> <span class="nx">user</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">    <span class="k">return</span> <span class="kc">true</span><span class="p">,</span> <span class="kc">nil</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>修改行為集中在 repository 內，lock、驗證與狀態一致性也留在同一個地方。</p>
<h2 id="策略mutex-和-channel-owner-要按資料形狀選擇">【策略】mutex 和 channel owner 要按資料形狀選擇</h2>
<p>狀態保護的核心選擇是 mutex owner 或 goroutine owner。兩者都符合 Go 的精神，差異在資料存取模式。</p>
<table>
  <thead>
      <tr>
          <th>方法</th>
          <th>適用情境</th>
          <th>代價</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>mutex owner</td>
          <td>多個方法需要同步讀寫狀態</td>
          <td>要維護 lock 與 copy boundary</td>
      </tr>
      <tr>
          <td>goroutine owner</td>
          <td>所有修改都能表示成訊息</td>
          <td>要設計 command、reply、shutdown</td>
      </tr>
  </tbody>
</table>
<p>Mutex 版本：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="ln">1</span><span class="cl"><span class="nx">repo</span><span class="p">.</span><span class="nf">Save</span><span class="p">(</span><span class="nx">ctx</span><span class="p">,</span> <span class="nx">user</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="nx">user</span><span class="p">,</span> <span class="nx">ok</span><span class="p">,</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">repo</span><span class="p">.</span><span class="nf">Find</span><span class="p">(</span><span class="nx">ctx</span><span class="p">,</span> <span class="nx">id</span><span class="p">)</span></span></span></code></pre></div><p>Goroutine owner 版本：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="ln">1</span><span class="cl"><span class="kd">type</span> <span class="nx">command</span> <span class="kd">struct</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">    <span class="nx">kind</span>  <span class="kt">string</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">    <span class="nx">user</span>  <span class="nx">User</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">    <span class="nx">reply</span> <span class="kd">chan</span> <span class="nx">result</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>不要為了避免 mutex 而把簡單狀態硬改成複雜訊息系統。也不要在需要嚴格順序與單一事件流時到處加 lock。選擇應該來自資料形狀與讀寫模式。</p>
<h2 id="測試copy-boundary-要用外部修改驗證">【測試】copy boundary 要用外部修改驗證</h2>
<p>Copy boundary 的測試核心是呼叫 getter 後修改回傳值，再確認 owner 內部資料沒有被改動。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="kd">func</span> <span class="nf">TestUsersReturnsCopy</span><span class="p">(</span><span class="nx">t</span> <span class="o">*</span><span class="nx">testing</span><span class="p">.</span><span class="nx">T</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">    <span class="nx">repo</span> <span class="o">:=</span> <span class="nf">NewUserRepository</span><span class="p">()</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">    <span class="nx">ctx</span> <span class="o">:=</span> <span class="nx">context</span><span class="p">.</span><span class="nf">Background</span><span class="p">()</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">    <span class="k">if</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">repo</span><span class="p">.</span><span class="nf">Save</span><span class="p">(</span><span class="nx">ctx</span><span class="p">,</span> <span class="nx">User</span><span class="p">{</span><span class="nx">ID</span><span class="p">:</span> <span class="s">&#34;user_1&#34;</span><span class="p">,</span> <span class="nx">Email</span><span class="p">:</span> <span class="s">&#34;a@example.com&#34;</span><span class="p">});</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">        <span class="nx">t</span><span class="p">.</span><span class="nf">Fatalf</span><span class="p">(</span><span class="s">&#34;save user: %v&#34;</span><span class="p">,</span> <span class="nx">err</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">    <span class="nx">users</span><span class="p">,</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">repo</span><span class="p">.</span><span class="nf">Users</span><span class="p">(</span><span class="nx">ctx</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">    <span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">        <span class="nx">t</span><span class="p">.</span><span class="nf">Fatalf</span><span class="p">(</span><span class="s">&#34;users: %v&#34;</span><span class="p">,</span> <span class="nx">err</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">    <span class="nb">delete</span><span class="p">(</span><span class="nx">users</span><span class="p">,</span> <span class="s">&#34;user_1&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">
</span></span><span class="line"><span class="ln">15</span><span class="cl">    <span class="nx">_</span><span class="p">,</span> <span class="nx">ok</span><span class="p">,</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">repo</span><span class="p">.</span><span class="nf">Find</span><span class="p">(</span><span class="nx">ctx</span><span class="p">,</span> <span class="s">&#34;user_1&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">    <span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl">        <span class="nx">t</span><span class="p">.</span><span class="nf">Fatalf</span><span class="p">(</span><span class="s">&#34;find user: %v&#34;</span><span class="p">,</span> <span class="nx">err</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln">19</span><span class="cl">    <span class="k">if</span> <span class="p">!</span><span class="nx">ok</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">20</span><span class="cl">        <span class="nx">t</span><span class="p">.</span><span class="nf">Fatalf</span><span class="p">(</span><span class="s">&#34;repository should not be modified through returned map&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">21</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln">22</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>Slice copy 也用同樣方式測：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="kd">func</span> <span class="nf">TestEventListReturnsCopy</span><span class="p">(</span><span class="nx">t</span> <span class="o">*</span><span class="nx">testing</span><span class="p">.</span><span class="nx">T</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">    <span class="nx">events</span> <span class="o">:=</span> <span class="o">&amp;</span><span class="nx">RecentEvents</span><span class="p">{}</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">    <span class="nx">ctx</span> <span class="o">:=</span> <span class="nx">context</span><span class="p">.</span><span class="nf">Background</span><span class="p">()</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">    <span class="nx">events</span><span class="p">.</span><span class="nf">Append</span><span class="p">(</span><span class="nx">ctx</span><span class="p">,</span> <span class="nx">Event</span><span class="p">{</span><span class="nx">ID</span><span class="p">:</span> <span class="s">&#34;evt_1&#34;</span><span class="p">})</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">    <span class="nx">got</span> <span class="o">:=</span> <span class="nx">events</span><span class="p">.</span><span class="nf">List</span><span class="p">(</span><span class="nx">ctx</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">    <span class="nx">got</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="nx">ID</span> <span class="p">=</span> <span class="s">&#34;changed&#34;</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">    <span class="nx">again</span> <span class="o">:=</span> <span class="nx">events</span><span class="p">.</span><span class="nf">List</span><span class="p">(</span><span class="nx">ctx</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">    <span class="k">if</span> <span class="nx">again</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="nx">ID</span> <span class="o">!=</span> <span class="s">&#34;evt_1&#34;</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">        <span class="nx">t</span><span class="p">.</span><span class="nf">Fatalf</span><span class="p">(</span><span class="s">&#34;internal event was modified through returned slice&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>這類測試能直接防止未來有人為了「省 copy」而破壞狀態邊界。</p>
<h2 id="測試race-detector-驗證同步邊界">【測試】race detector 驗證同步邊界</h2>
<p>Race detector 的核心用途是找出未同步的共享記憶體存取。對含有 goroutine、map、slice、repository 的測試，應定期執行：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">go <span class="nb">test</span> -race ./...</span></span></code></pre></div><p>可以用併發測試增加觸發機率：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="kd">func</span> <span class="nf">TestRepositoryConcurrentAccess</span><span class="p">(</span><span class="nx">t</span> <span class="o">*</span><span class="nx">testing</span><span class="p">.</span><span class="nx">T</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">    <span class="nx">repo</span> <span class="o">:=</span> <span class="nf">NewUserRepository</span><span class="p">()</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">    <span class="nx">ctx</span> <span class="o">:=</span> <span class="nx">context</span><span class="p">.</span><span class="nf">Background</span><span class="p">()</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">    <span class="kd">var</span> <span class="nx">wg</span> <span class="nx">sync</span><span class="p">.</span><span class="nx">WaitGroup</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">    <span class="k">for</span> <span class="nx">i</span> <span class="o">:=</span> <span class="mi">0</span><span class="p">;</span> <span class="nx">i</span> <span class="p">&lt;</span> <span class="mi">100</span><span class="p">;</span> <span class="nx">i</span><span class="o">++</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">        <span class="nx">i</span> <span class="o">:=</span> <span class="nx">i</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">        <span class="nx">wg</span><span class="p">.</span><span class="nf">Add</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">        <span class="k">go</span> <span class="kd">func</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">            <span class="k">defer</span> <span class="nx">wg</span><span class="p">.</span><span class="nf">Done</span><span class="p">()</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">            <span class="nx">id</span> <span class="o">:=</span> <span class="nx">fmt</span><span class="p">.</span><span class="nf">Sprintf</span><span class="p">(</span><span class="s">&#34;user_%d&#34;</span><span class="p">,</span> <span class="nx">i</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">            <span class="nx">_</span> <span class="p">=</span> <span class="nx">repo</span><span class="p">.</span><span class="nf">Save</span><span class="p">(</span><span class="nx">ctx</span><span class="p">,</span> <span class="nx">User</span><span class="p">{</span><span class="nx">ID</span><span class="p">:</span> <span class="nx">id</span><span class="p">})</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">            <span class="nx">_</span><span class="p">,</span> <span class="nx">_</span><span class="p">,</span> <span class="nx">_</span> <span class="p">=</span> <span class="nx">repo</span><span class="p">.</span><span class="nf">Find</span><span class="p">(</span><span class="nx">ctx</span><span class="p">,</span> <span class="nx">id</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">        <span class="p">}()</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">    <span class="nx">wg</span><span class="p">.</span><span class="nf">Wait</span><span class="p">()</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>這個測試本身不一定能證明沒有所有問題，但搭配 <code>-race</code> 可以檢查 repository 方法是否真的包住共享 map。</p>
<h2 id="本章不處理">本章不處理</h2>
<p>本章先聚焦單一 Go process 內的共享狀態保護；更外層的資料庫交易、快取一致性與資料複製邊界，會在下列章節再往外延伸：</p>
<ul>
<li><a href="/blog/backend/01-database/" data-link-title="模組一：資料庫與持久化" data-link-desc="整理 SQL、transaction、migration 與 repository adapter 的後端實務">Backend：資料庫與持久化</a></li>
<li><a href="/blog/backend/02-cache-redis/" data-link-title="模組二：快取與 Redis" data-link-desc="整理快取策略、Redis 資料型別與分散式狀態輔助能力">Backend：快取與 Redis</a></li>
<li><a href="/blog/go/02-types-data/pointers-copy/" data-link-title="2.5 指標與資料複製邊界" data-link-desc="理解指標、slice 與共享狀態的防護策略">Go 入門：指標與資料複製邊界</a></li>
</ul>
<h2 id="和-go-教材的關係">和 Go 教材的關係</h2>
<p>這一章承接的是 repository、copy boundary 與 state owner；如果你要先回看語言教材，可以讀：</p>
<ul>
<li><a href="/blog/go/06-practical/repository-port/" data-link-title="6.6 如何新增 repository port" data-link-desc="先建立儲存邊界，再決定 memory、SQLite 或外部資料庫實作">Go：如何新增 repository port</a></li>
<li><a href="/blog/go/06-practical/state-fields/" data-link-title="6.3 如何擴展狀態投影欄位" data-link-desc="更新狀態模型、repository 與 API 輸出">Go：如何擴展狀態投影欄位</a></li>
<li><a href="/blog/go/07-refactoring/state-boundary/" data-link-title="7.4 狀態管理的安全邊界" data-link-desc="用 lock、copy 與 API 限制保護共享狀態">Go：狀態管理的安全邊界</a></li>
<li><a href="/blog/go/07-refactoring/interface-boundary/" data-link-title="7.2 用 interface 隔離外部依賴" data-link-desc="建立小而穩定的測試替身">Go：用 interface 隔離外部依賴</a></li>
</ul>
<h2 id="小結">小結</h2>
<p>共享狀態的安全邊界由 owner、lock、copy 與明確修改方法組成。Map/slice 讀寫要經過同一個 owner；getter 不應暴露內部可變資料；含 map、slice、pointer 的 struct 要考慮 deep copy；修改行為應集中在方法內。這些規則能讓長時間運行的 Go 服務避開 data race、外部突變與難以重現的狀態錯誤。</p>
]]></content:encoded></item><item><title>2.4 慢客戶端與 send buffer 管理</title><link>https://tarrragon.github.io/blog/go-advanced/02-networking-websocket/slow-client/</link><pubDate>Wed, 22 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/go-advanced/02-networking-websocket/slow-client/</guid><description>&lt;p>慢客戶端管理的核心問題是單一 client 的讀取速度可能低於 server 推送速度。若 send &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/buffer/" data-link-title="Buffer" data-link-desc="說明系統如何用暫存空間吸收短暫速度差與尖峰流量">buffer&lt;/a> 沒有上限，慢 client 會把訊息堆在記憶體裡；若 hub 使用 blocking send，慢 client 會拖住所有 client。&lt;/p>
&lt;h2 id="本章目標">本章目標&lt;/h2>
&lt;p>學完本章後，你將能夠：&lt;/p>
&lt;ol>
&lt;li>分辨慢 client 對 hub、write pump、記憶體的影響&lt;/li>
&lt;li>用 bounded send channel 限制單一 client 的排隊量&lt;/li>
&lt;li>設計 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/queue/" data-link-title="Queue" data-link-desc="說明 queue 如何保存等待處理的工作並形成容量邊界">queue&lt;/a> full 時的 drop、disconnect、coalesce 策略&lt;/li>
&lt;li>在必要時用 byte budget 管理大型 payload&lt;/li>
&lt;li>測試 send buffer 滿載與 client unregister 行為&lt;/li>
&lt;/ol>
&lt;hr>
&lt;h2 id="觀察慢-client-會把局部問題變成全域問題">【觀察】慢 client 會把局部問題變成全域問題&lt;/h2>
&lt;p>慢 client 的核心風險是它不只影響自己。若 hub broadcast 時對每個 client 使用 blocking send，其中一個 client 的 &lt;code>send&lt;/code> channel 滿了，hub 就可能卡住，其他 client 也收不到訊息。&lt;/p>
&lt;p>反模式：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-go" data-lang="go">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="kd">func&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">h&lt;/span> &lt;span class="o">*&lt;/span>&lt;span class="nx">Hub&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="nf">Broadcast&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">message&lt;/span> &lt;span class="nx">ServerMessage&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl"> &lt;span class="k">for&lt;/span> &lt;span class="nx">client&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="k">range&lt;/span> &lt;span class="nx">h&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">clients&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl"> &lt;span class="nx">client&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">send&lt;/span> &lt;span class="o">&amp;lt;-&lt;/span> &lt;span class="nx">message&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>這段程式看起來保證送達，但實際上把整個 hub 的可用性綁在最慢的 client 上。只要一個 client 不讀，所有 broadcast 都可能停住。&lt;/p>
&lt;h2 id="判讀send-channel-是每個-client-的容量邊界">【判讀】send channel 是每個 client 的容量邊界&lt;/h2>
&lt;p>Send channel 的核心責任是作為單一 client 的輸出佇列。它必須有容量上限，否則 server 會替慢 client 無限制保存訊息。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-go" data-lang="go">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="kd">const&lt;/span> &lt;span class="nx">sendBufferSize&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="mi">64&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl">&lt;span class="kd">type&lt;/span> &lt;span class="nx">Client&lt;/span> &lt;span class="kd">struct&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl"> &lt;span class="nx">id&lt;/span> &lt;span class="kt">string&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl"> &lt;span class="nx">send&lt;/span> &lt;span class="kd">chan&lt;/span> &lt;span class="nx">ServerMessage&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">&lt;span class="kd">func&lt;/span> &lt;span class="nf">NewClient&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">id&lt;/span> &lt;span class="kt">string&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">*&lt;/span>&lt;span class="nx">Client&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="o">&amp;amp;&lt;/span>&lt;span class="nx">Client&lt;/span>&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl"> &lt;span class="nx">id&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="nx">id&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl"> &lt;span class="nx">send&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="nb">make&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="kd">chan&lt;/span> &lt;span class="nx">ServerMessage&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">sendBufferSize&lt;/span>&lt;span class="p">),&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">13&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Buffer 的目的只是吸收短暫尖峰，不是讓 client 長期落後。若 client 長期消費速度低於推送速度，任何有限 buffer 都會滿。&lt;/p>
&lt;h2 id="策略滿載策略取決於訊息語意">【策略】滿載策略取決於訊息語意&lt;/h2>
&lt;p>慢 client 滿載的核心決策是訊息能不能遺失。不同資料類型需要不同策略。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>訊息類型&lt;/th>
 &lt;th>常見策略&lt;/th>
 &lt;th>理由&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>即時狀態 snapshot&lt;/td>
 &lt;td>可丟棄舊訊息或 coalesce&lt;/td>
 &lt;td>最新狀態比每個中間狀態重要&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>action result&lt;/td>
 &lt;td>優先送達，滿載時可斷線&lt;/td>
 &lt;td>client 需要知道操作結果&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>診斷 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/log/" data-link-title="Log" data-link-desc="說明 log 如何記錄單一事件的上下文並支援事故排查">log&lt;/a> stream&lt;/td>
 &lt;td>可取樣或丟棄&lt;/td>
 &lt;td>資料量大，通常不是唯一真相&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>金流、訂單、稽核事件&lt;/td>
 &lt;td>不應只靠 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/websocket/" data-link-title="WebSocket" data-link-desc="說明 WebSocket 如何提供長連線雙向即時通訊">WebSocket&lt;/a>&lt;/td>
 &lt;td>需要可靠儲存或可重播來源&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>WebSocket send buffer 不應承擔資料可靠性。若訊息不能遺失，可靠性應放在資料庫、queue 或 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/event-log/" data-link-title="Event Log" data-link-desc="說明事件歷史如何保存、重播與支援跨服務資料重建">event log&lt;/a>，WebSocket 只負責即時通知。&lt;/p>
&lt;h2 id="執行non-blocking-send-保護-hub">【執行】non-blocking send 保護 hub&lt;/h2>
&lt;p>Hub 的核心保護是 broadcast 時不被單一 client 阻塞。&lt;code>TrySend&lt;/code> 可以讓 hub 立即知道該 client 是否已滿載。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-go" data-lang="go">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="kd">func&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">c&lt;/span> &lt;span class="o">*&lt;/span>&lt;span class="nx">Client&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="nf">TrySend&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">message&lt;/span> &lt;span class="nx">ServerMessage&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="kt">bool&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl"> &lt;span class="k">select&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl"> &lt;span class="k">case&lt;/span> &lt;span class="nx">c&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">send&lt;/span> &lt;span class="o">&amp;lt;-&lt;/span> &lt;span class="nx">message&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="kc">true&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl"> &lt;span class="k">default&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="kc">false&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">8&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Hub 可以把滿載 client 送進 unregister：&lt;/p></description><content:encoded><![CDATA[<p>慢客戶端管理的核心問題是單一 client 的讀取速度可能低於 server 推送速度。若 send <a href="/blog/backend/knowledge-cards/buffer/" data-link-title="Buffer" data-link-desc="說明系統如何用暫存空間吸收短暫速度差與尖峰流量">buffer</a> 沒有上限，慢 client 會把訊息堆在記憶體裡；若 hub 使用 blocking send，慢 client 會拖住所有 client。</p>
<h2 id="本章目標">本章目標</h2>
<p>學完本章後，你將能夠：</p>
<ol>
<li>分辨慢 client 對 hub、write pump、記憶體的影響</li>
<li>用 bounded send channel 限制單一 client 的排隊量</li>
<li>設計 <a href="/blog/backend/knowledge-cards/queue/" data-link-title="Queue" data-link-desc="說明 queue 如何保存等待處理的工作並形成容量邊界">queue</a> full 時的 drop、disconnect、coalesce 策略</li>
<li>在必要時用 byte budget 管理大型 payload</li>
<li>測試 send buffer 滿載與 client unregister 行為</li>
</ol>
<hr>
<h2 id="觀察慢-client-會把局部問題變成全域問題">【觀察】慢 client 會把局部問題變成全域問題</h2>
<p>慢 client 的核心風險是它不只影響自己。若 hub broadcast 時對每個 client 使用 blocking send，其中一個 client 的 <code>send</code> channel 滿了，hub 就可能卡住，其他 client 也收不到訊息。</p>
<p>反模式：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="ln">1</span><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">h</span> <span class="o">*</span><span class="nx">Hub</span><span class="p">)</span> <span class="nf">Broadcast</span><span class="p">(</span><span class="nx">message</span> <span class="nx">ServerMessage</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">    <span class="k">for</span> <span class="nx">client</span> <span class="o">:=</span> <span class="k">range</span> <span class="nx">h</span><span class="p">.</span><span class="nx">clients</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">        <span class="nx">client</span><span class="p">.</span><span class="nx">send</span> <span class="o">&lt;-</span> <span class="nx">message</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>這段程式看起來保證送達，但實際上把整個 hub 的可用性綁在最慢的 client 上。只要一個 client 不讀，所有 broadcast 都可能停住。</p>
<h2 id="判讀send-channel-是每個-client-的容量邊界">【判讀】send channel 是每個 client 的容量邊界</h2>
<p>Send channel 的核心責任是作為單一 client 的輸出佇列。它必須有容量上限，否則 server 會替慢 client 無限制保存訊息。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="kd">const</span> <span class="nx">sendBufferSize</span> <span class="p">=</span> <span class="mi">64</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="kd">type</span> <span class="nx">Client</span> <span class="kd">struct</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">    <span class="nx">id</span>   <span class="kt">string</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">    <span class="nx">send</span> <span class="kd">chan</span> <span class="nx">ServerMessage</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="kd">func</span> <span class="nf">NewClient</span><span class="p">(</span><span class="nx">id</span> <span class="kt">string</span><span class="p">)</span> <span class="o">*</span><span class="nx">Client</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">    <span class="k">return</span> <span class="o">&amp;</span><span class="nx">Client</span><span class="p">{</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">        <span class="nx">id</span><span class="p">:</span>   <span class="nx">id</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">        <span class="nx">send</span><span class="p">:</span> <span class="nb">make</span><span class="p">(</span><span class="kd">chan</span> <span class="nx">ServerMessage</span><span class="p">,</span> <span class="nx">sendBufferSize</span><span class="p">),</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>Buffer 的目的只是吸收短暫尖峰，不是讓 client 長期落後。若 client 長期消費速度低於推送速度，任何有限 buffer 都會滿。</p>
<h2 id="策略滿載策略取決於訊息語意">【策略】滿載策略取決於訊息語意</h2>
<p>慢 client 滿載的核心決策是訊息能不能遺失。不同資料類型需要不同策略。</p>
<table>
  <thead>
      <tr>
          <th>訊息類型</th>
          <th>常見策略</th>
          <th>理由</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>即時狀態 snapshot</td>
          <td>可丟棄舊訊息或 coalesce</td>
          <td>最新狀態比每個中間狀態重要</td>
      </tr>
      <tr>
          <td>action result</td>
          <td>優先送達，滿載時可斷線</td>
          <td>client 需要知道操作結果</td>
      </tr>
      <tr>
          <td>診斷 <a href="/blog/backend/knowledge-cards/log/" data-link-title="Log" data-link-desc="說明 log 如何記錄單一事件的上下文並支援事故排查">log</a> stream</td>
          <td>可取樣或丟棄</td>
          <td>資料量大，通常不是唯一真相</td>
      </tr>
      <tr>
          <td>金流、訂單、稽核事件</td>
          <td>不應只靠 <a href="/blog/backend/knowledge-cards/websocket/" data-link-title="WebSocket" data-link-desc="說明 WebSocket 如何提供長連線雙向即時通訊">WebSocket</a></td>
          <td>需要可靠儲存或可重播來源</td>
      </tr>
  </tbody>
</table>
<p>WebSocket send buffer 不應承擔資料可靠性。若訊息不能遺失，可靠性應放在資料庫、queue 或 <a href="/blog/backend/knowledge-cards/event-log/" data-link-title="Event Log" data-link-desc="說明事件歷史如何保存、重播與支援跨服務資料重建">event log</a>，WebSocket 只負責即時通知。</p>
<h2 id="執行non-blocking-send-保護-hub">【執行】non-blocking send 保護 hub</h2>
<p>Hub 的核心保護是 broadcast 時不被單一 client 阻塞。<code>TrySend</code> 可以讓 hub 立即知道該 client 是否已滿載。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="ln">1</span><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">c</span> <span class="o">*</span><span class="nx">Client</span><span class="p">)</span> <span class="nf">TrySend</span><span class="p">(</span><span class="nx">message</span> <span class="nx">ServerMessage</span><span class="p">)</span> <span class="kt">bool</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">    <span class="k">select</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">    <span class="k">case</span> <span class="nx">c</span><span class="p">.</span><span class="nx">send</span> <span class="o">&lt;-</span> <span class="nx">message</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">        <span class="k">return</span> <span class="kc">true</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">    <span class="k">default</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">        <span class="k">return</span> <span class="kc">false</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>Hub 可以把滿載 client 送進 unregister：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">h</span> <span class="o">*</span><span class="nx">Hub</span><span class="p">)</span> <span class="nf">Broadcast</span><span class="p">(</span><span class="nx">topic</span> <span class="kt">string</span><span class="p">,</span> <span class="nx">message</span> <span class="nx">ServerMessage</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">    <span class="k">for</span> <span class="nx">client</span> <span class="o">:=</span> <span class="k">range</span> <span class="nx">h</span><span class="p">.</span><span class="nx">clients</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">        <span class="k">if</span> <span class="p">!</span><span class="nx">client</span><span class="p">.</span><span class="nf">IsSubscribed</span><span class="p">(</span><span class="nx">topic</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">            <span class="k">continue</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">        <span class="p">}</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">        <span class="k">if</span> <span class="nx">ok</span> <span class="o">:=</span> <span class="nx">client</span><span class="p">.</span><span class="nf">TrySend</span><span class="p">(</span><span class="nx">message</span><span class="p">);</span> <span class="p">!</span><span class="nx">ok</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">            <span class="nx">h</span><span class="p">.</span><span class="nx">unregister</span> <span class="o">&lt;-</span> <span class="nx">client</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">        <span class="p">}</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>這種策略犧牲慢 client，保護整體服務。對即時通知服務來說，讓慢 client 重連並重新取得 snapshot，通常比讓所有 client 等它更合理。</p>
<h2 id="策略drop-newestdrop-oldestdisconnect-是不同語意">【策略】drop newest、drop oldest、disconnect 是不同語意</h2>
<p>Queue full 策略的核心差異是保留哪一筆資料，以及是否繼續維持連線。</p>
<table>
  <thead>
      <tr>
          <th>策略</th>
          <th>行為</th>
          <th>適用情境</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>drop newest</td>
          <td>新訊息不進 queue</td>
          <td>舊訊息仍有價值</td>
      </tr>
      <tr>
          <td>drop oldest</td>
          <td>移除舊訊息，保留最新</td>
          <td>狀態型更新</td>
      </tr>
      <tr>
          <td>disconnect</td>
          <td>關閉 client，要求重連</td>
          <td>client 已明顯跟不上</td>
      </tr>
      <tr>
          <td>coalesce</td>
          <td>合併多筆更新成一筆</td>
          <td><a href="/blog/backend/knowledge-cards/topic/" data-link-title="Topic" data-link-desc="說明 topic 如何把事件依主題分流給不同訂閱者">topic</a> 最新狀態可覆蓋</td>
      </tr>
  </tbody>
</table>
<p>Drop oldest 範例：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">c</span> <span class="o">*</span><span class="nx">Client</span><span class="p">)</span> <span class="nf">TrySendLatest</span><span class="p">(</span><span class="nx">message</span> <span class="nx">ServerMessage</span><span class="p">)</span> <span class="kt">bool</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">    <span class="k">select</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">    <span class="k">case</span> <span class="nx">c</span><span class="p">.</span><span class="nx">send</span> <span class="o">&lt;-</span> <span class="nx">message</span><span class="p">:</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">        <span class="k">return</span> <span class="kc">true</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">    <span class="k">default</span><span class="p">:</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">    <span class="k">select</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">    <span class="k">case</span> <span class="o">&lt;-</span><span class="nx">c</span><span class="p">.</span><span class="nx">send</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">    <span class="k">default</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">
</span></span><span class="line"><span class="ln">13</span><span class="cl">    <span class="k">select</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">    <span class="k">case</span> <span class="nx">c</span><span class="p">.</span><span class="nx">send</span> <span class="o">&lt;-</span> <span class="nx">message</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">        <span class="k">return</span> <span class="kc">true</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">    <span class="k">default</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl">        <span class="k">return</span> <span class="kc">false</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln">19</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>這段程式表示「新狀態比舊狀態重要」。它不適合 action result 或不可遺失事件，因為它會主動丟掉尚未送出的舊訊息。</p>
<h2 id="策略byte-budget-比-message-count-更接近記憶體風險">【策略】byte budget 比 message count 更接近記憶體風險</h2>
<p>Message count 的核心限制是每筆訊息大小不同。64 筆小訊息和 64 筆大型 JSON payload 的記憶體成本差很多；當 payload 大小差異明顯時，可以加上 byte budget。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="kd">type</span> <span class="nx">Client</span> <span class="kd">struct</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">    <span class="nx">send</span>      <span class="kd">chan</span> <span class="nx">ServerMessage</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">    <span class="nx">sendBytes</span> <span class="kt">int64</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">    <span class="nx">maxBytes</span>  <span class="kt">int64</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">c</span> <span class="o">*</span><span class="nx">Client</span><span class="p">)</span> <span class="nf">TrySend</span><span class="p">(</span><span class="nx">message</span> <span class="nx">ServerMessage</span><span class="p">)</span> <span class="kt">bool</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">    <span class="nx">size</span> <span class="o">:=</span> <span class="nb">int64</span><span class="p">(</span><span class="nx">message</span><span class="p">.</span><span class="nf">Size</span><span class="p">())</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">    <span class="k">if</span> <span class="nx">atomic</span><span class="p">.</span><span class="nf">AddInt64</span><span class="p">(</span><span class="o">&amp;</span><span class="nx">c</span><span class="p">.</span><span class="nx">sendBytes</span><span class="p">,</span> <span class="nx">size</span><span class="p">)</span> <span class="p">&gt;</span> <span class="nx">c</span><span class="p">.</span><span class="nx">maxBytes</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">        <span class="nx">atomic</span><span class="p">.</span><span class="nf">AddInt64</span><span class="p">(</span><span class="o">&amp;</span><span class="nx">c</span><span class="p">.</span><span class="nx">sendBytes</span><span class="p">,</span> <span class="o">-</span><span class="nx">size</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">        <span class="k">return</span> <span class="kc">false</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">
</span></span><span class="line"><span class="ln">14</span><span class="cl">    <span class="k">select</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">    <span class="k">case</span> <span class="nx">c</span><span class="p">.</span><span class="nx">send</span> <span class="o">&lt;-</span> <span class="nx">message</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">        <span class="k">return</span> <span class="kc">true</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl">    <span class="k">default</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl">        <span class="nx">atomic</span><span class="p">.</span><span class="nf">AddInt64</span><span class="p">(</span><span class="o">&amp;</span><span class="nx">c</span><span class="p">.</span><span class="nx">sendBytes</span><span class="p">,</span> <span class="o">-</span><span class="nx">size</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">19</span><span class="cl">        <span class="k">return</span> <span class="kc">false</span>
</span></span><span class="line"><span class="ln">20</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln">21</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>Write pump 成功取出並寫出訊息後，必須扣回 byte budget：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="ln">1</span><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">c</span> <span class="o">*</span><span class="nx">Client</span><span class="p">)</span> <span class="nf">markSent</span><span class="p">(</span><span class="nx">message</span> <span class="nx">ServerMessage</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">    <span class="nx">atomic</span><span class="p">.</span><span class="nf">AddInt64</span><span class="p">(</span><span class="o">&amp;</span><span class="nx">c</span><span class="p">.</span><span class="nx">sendBytes</span><span class="p">,</span> <span class="o">-</span><span class="nb">int64</span><span class="p">(</span><span class="nx">message</span><span class="p">.</span><span class="nf">Size</span><span class="p">()))</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>Byte budget 更接近記憶體風險，但也更複雜。只有在訊息大小差異大、或服務連線數高時才值得加入；小型服務先用固定 buffer 通常足夠。</p>
<h2 id="判讀write-pump-慢不一定是-client-的錯">【判讀】write pump 慢不一定是 client 的錯</h2>
<p>慢寫入的核心原因可能在 client，也可能在 server。Client 網路慢、瀏覽器停住、行動裝置休眠會造成慢寫；server payload 太大、序列化太慢、單次寫入沒有 <a href="/blog/backend/knowledge-cards/deadline/" data-link-title="Deadline" data-link-desc="說明整體操作的截止時間如何沿著服務邊界傳遞">deadline</a> 也會造成問題。</p>
<p>排查方向：</p>
<ul>
<li>send buffer 長期接近滿載</li>
<li>write deadline 錯誤增加</li>
<li>單筆 message size 過大</li>
<li>broadcast 頻率超過 client 消費能力</li>
<li>某些 topic 推送量異常高</li>
</ul>
<p>queue full 的歸因應同時檢查 client 與 server 端訊號。若所有 client 都慢，通常是 server 推送量、payload 大小或下游網路策略出問題。</p>
<h2 id="策略滿載要有觀測欄位">【策略】滿載要有觀測欄位</h2>
<p>慢 client 策略的核心要求是可觀測。若系統選擇 drop 或 disconnect，應記錄足夠欄位讓工程師知道原因。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">h</span> <span class="o">*</span><span class="nx">Hub</span><span class="p">)</span> <span class="nf">handleFullClient</span><span class="p">(</span><span class="nx">client</span> <span class="o">*</span><span class="nx">Client</span><span class="p">,</span> <span class="nx">topic</span> <span class="kt">string</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">    <span class="nx">metrics</span><span class="p">.</span><span class="nf">Inc</span><span class="p">(</span><span class="s">&#34;websocket_client_send_full&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">    <span class="nx">h</span><span class="p">.</span><span class="nx">logger</span><span class="p">.</span><span class="nf">Warn</span><span class="p">(</span><span class="s">&#34;websocket client send buffer full&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">        <span class="s">&#34;client_id&#34;</span><span class="p">,</span> <span class="nx">client</span><span class="p">.</span><span class="nf">ID</span><span class="p">(),</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">        <span class="s">&#34;topic&#34;</span><span class="p">,</span> <span class="nx">topic</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">        <span class="s">&#34;send_queue_len&#34;</span><span class="p">,</span> <span class="nb">len</span><span class="p">(</span><span class="nx">client</span><span class="p">.</span><span class="nx">send</span><span class="p">),</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">        <span class="s">&#34;send_queue_cap&#34;</span><span class="p">,</span> <span class="nb">cap</span><span class="p">(</span><span class="nx">client</span><span class="p">.</span><span class="nx">send</span><span class="p">),</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">    <span class="p">)</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">    <span class="nx">h</span><span class="p">.</span><span class="nx">unregister</span> <span class="o">&lt;-</span> <span class="nx">client</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>Log 用來追單次事件，metric 用來看趨勢。若滿載數量突然增加，可能是某個 topic 推送量上升，也可能是 client 版本或網路環境改變。</p>
<h2 id="測試滿載測試要先填滿-buffer">【測試】滿載測試要先填滿 buffer</h2>
<p>慢 client 測試的核心是直接建立滿載條件。容量為 1 的 channel 加上預先填滿的資料，可以穩定製造 queue full；sleep 只是在等待排程運氣。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="kd">func</span> <span class="nf">TestTrySendReturnsFalseWhenBufferFull</span><span class="p">(</span><span class="nx">t</span> <span class="o">*</span><span class="nx">testing</span><span class="p">.</span><span class="nx">T</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">    <span class="nx">client</span> <span class="o">:=</span> <span class="o">&amp;</span><span class="nx">Client</span><span class="p">{</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">        <span class="nx">id</span><span class="p">:</span>   <span class="s">&#34;client_1&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">        <span class="nx">send</span><span class="p">:</span> <span class="nb">make</span><span class="p">(</span><span class="kd">chan</span> <span class="nx">ServerMessage</span><span class="p">,</span> <span class="mi">1</span><span class="p">),</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">    <span class="nx">client</span><span class="p">.</span><span class="nx">send</span> <span class="o">&lt;-</span> <span class="nx">ServerMessage</span><span class="p">{</span><span class="nx">Type</span><span class="p">:</span> <span class="s">&#34;first&#34;</span><span class="p">}</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">    <span class="nx">ok</span> <span class="o">:=</span> <span class="nx">client</span><span class="p">.</span><span class="nf">TrySend</span><span class="p">(</span><span class="nx">ServerMessage</span><span class="p">{</span><span class="nx">Type</span><span class="p">:</span> <span class="s">&#34;second&#34;</span><span class="p">})</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">    <span class="k">if</span> <span class="nx">ok</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">        <span class="nx">t</span><span class="p">.</span><span class="nf">Fatalf</span><span class="p">(</span><span class="s">&#34;TrySend should return false when buffer is full&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>Hub unregister 行為也可以測：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="kd">func</span> <span class="nf">TestBroadcastUnregistersFullClient</span><span class="p">(</span><span class="nx">t</span> <span class="o">*</span><span class="nx">testing</span><span class="p">.</span><span class="nx">T</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">    <span class="nx">hub</span> <span class="o">:=</span> <span class="nf">NewHub</span><span class="p">()</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">    <span class="nx">client</span> <span class="o">:=</span> <span class="nf">NewTestClient</span><span class="p">(</span><span class="s">&#34;client_1&#34;</span><span class="p">,</span> <span class="mi">1</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">    <span class="nx">client</span><span class="p">.</span><span class="nf">Subscribe</span><span class="p">(</span><span class="s">&#34;alerts&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">    <span class="nx">client</span><span class="p">.</span><span class="nx">send</span> <span class="o">&lt;-</span> <span class="nx">ServerMessage</span><span class="p">{</span><span class="nx">Type</span><span class="p">:</span> <span class="s">&#34;existing&#34;</span><span class="p">}</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">    <span class="nx">hub</span><span class="p">.</span><span class="nx">clients</span><span class="p">[</span><span class="nx">client</span><span class="p">]</span> <span class="p">=</span> <span class="kd">struct</span><span class="p">{}{}</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">    <span class="nx">hub</span><span class="p">.</span><span class="nf">Broadcast</span><span class="p">(</span><span class="s">&#34;alerts&#34;</span><span class="p">,</span> <span class="nx">ServerMessage</span><span class="p">{</span><span class="nx">Type</span><span class="p">:</span> <span class="s">&#34;new&#34;</span><span class="p">})</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">
</span></span><span class="line"><span class="ln">10</span><span class="cl">    <span class="k">select</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">    <span class="k">case</span> <span class="nx">got</span> <span class="o">:=</span> <span class="o">&lt;-</span><span class="nx">hub</span><span class="p">.</span><span class="nx">unregister</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">        <span class="k">if</span> <span class="nx">got</span> <span class="o">!=</span> <span class="nx">client</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">            <span class="nx">t</span><span class="p">.</span><span class="nf">Fatalf</span><span class="p">(</span><span class="s">&#34;unregister client mismatch&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">        <span class="p">}</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">    <span class="k">default</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">        <span class="nx">t</span><span class="p">.</span><span class="nf">Fatalf</span><span class="p">(</span><span class="s">&#34;full client should be unregistered&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>這類測試直接驗證服務策略：client 滿載時，hub 不阻塞，而是走指定降級路徑。</p>
<h2 id="本章不處理">本章不處理</h2>
<p>本章先處理單一 server 內的慢 client 與 send buffer 邊界；跨節點 <a href="/blog/backend/knowledge-cards/fan-out/" data-link-title="Fan-out" data-link-desc="說明單一事件同時分發給多個下游的訊息拓撲">fan-out</a> 與持久化同步，會在下列章節延伸：</p>
<ul>
<li><a href="/blog/go-advanced/07-distributed-operations/cross-node-websocket/" data-link-title="7.3 跨節點 WebSocket、presence 與重連協定" data-link-desc="把單一 server 的 WebSocket hub 擴展到多節點推送與連線狀態">Go 進階：跨節點 WebSocket、presence 與重連協定</a></li>
</ul>
<h2 id="和-go-教材的關係">和 Go 教材的關係</h2>
<p>這一章承接的是 channel <a href="/blog/backend/knowledge-cards/backpressure/" data-link-title="Backpressure" data-link-desc="說明下游處理速度不足時系統如何讓上游依下游能力送出工作">backpressure</a> 、non-blocking send 與 rate limiting；如果你要先回看語言教材，可以讀：</p>
<ul>
<li><a href="/blog/go/04-concurrency/channel/" data-link-title="4.2 channel：資料傳遞與 backpressure " data-link-desc="理解 channel 如何在 goroutine 之間傳遞資料並形成 backpressure ">Go：channel：資料傳遞與 backpressure </a></li>
<li><a href="/blog/go-advanced/01-concurrency-patterns/non-blocking-send/" data-link-title="1.3 非阻塞送出與事件丟棄策略" data-link-desc="設計 channel 滿載時的服務行為">Go：非阻塞送出與事件丟棄策略</a></li>
<li><a href="/blog/backend/knowledge-cards/rate-limit/" data-link-title="Rate Limit" data-link-desc="說明限流如何保護服務入口、下游依賴與租戶公平性">Go：rate limiting 與 backpressure </a></li>
<li><a href="/blog/backend/knowledge-cards/worker-pool/" data-link-title="Worker Pool" data-link-desc="說明一組 worker 如何限制同時處理量並保護下游資源">Go：bounded worker pool</a></li>
<li><a href="/blog/backend/03-message-queue/" data-link-title="模組三：訊息佇列與事件傳遞" data-link-desc="整理 durable queue、broker、retry、outbox 與 idempotency 的後端實務">Backend：訊息佇列與事件傳遞</a></li>
<li><a href="/blog/backend/02-cache-redis/" data-link-title="模組二：快取與 Redis" data-link-desc="整理快取策略、Redis 資料型別與分散式狀態輔助能力">Backend：快取與 Redis</a></li>
</ul>
<h2 id="小結">小結</h2>
<p>慢客戶端是 WebSocket 服務的容量控制問題。每個 client 的 send buffer 必須有上限，hub broadcast 不應被單一 client 阻塞，queue full 策略要符合訊息語意。必要時可加入 byte budget，但更重要的是明確決定 drop、disconnect、coalesce 或可靠儲存，並用 log、metric、測試讓降級行為可見。</p>
]]></content:encoded></item><item><title>3.4 資料結構與 allocation 壓力</title><link>https://tarrragon.github.io/blog/go-advanced/03-runtime-profiling/allocation/</link><pubDate>Wed, 22 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/go-advanced/03-runtime-profiling/allocation/</guid><description>&lt;p>Allocation 分析的核心目標是區分必要的安全複製與可優化的重複配置。Go 服務中很多配置來自 slice 成長、map/list 複製、JSON marshal、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/buffer/" data-link-title="Buffer" data-link-desc="說明系統如何用暫存空間吸收短暫速度差與尖峰流量">buffer&lt;/a> 建立與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/websocket/" data-link-title="WebSocket" data-link-desc="說明 WebSocket 如何提供長連線雙向即時通訊">WebSocket&lt;/a> payload；優化前要先確認配置是否位於熱路徑，且不能破壞狀態邊界。&lt;/p>
&lt;h2 id="本章目標">本章目標&lt;/h2>
&lt;p>學完本章後，你將能夠：&lt;/p>
&lt;ol>
&lt;li>理解 allocation 如何增加 GC 壓力&lt;/li>
&lt;li>分辨必要 copy boundary 與不必要重複配置&lt;/li>
&lt;li>用預配置降低 slice 成長成本&lt;/li>
&lt;li>判斷 JSON marshal 與 WebSocket payload 的重用邊界&lt;/li>
&lt;li>用 pprof 的 &lt;code>alloc_space&lt;/code> 與 &lt;code>inuse_space&lt;/code> 決定優化方向&lt;/li>
&lt;/ol>
&lt;hr>
&lt;h2 id="觀察allocation-壓力會放大-gc-成本">【觀察】allocation 壓力會放大 GC 成本&lt;/h2>
&lt;p>Allocation 的核心影響是增加 heap 成長速度，進而增加 GC 工作量。即使物件很快被回收，大量短命配置仍可能造成 CPU 與 latency 壓力。&lt;/p>
&lt;p>常見熱路徑：&lt;/p>
&lt;ul>
&lt;li>每次 WebSocket broadcast 都對每個 client 重新 marshal。&lt;/li>
&lt;li>每次 API list 都建立大型 slice。&lt;/li>
&lt;li>每次 repository 查詢都 copy 大型 map。&lt;/li>
&lt;li>每次 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/log/" data-link-title="Log" data-link-desc="說明 log 如何記錄單一事件的上下文並支援事故排查">log&lt;/a> 都組大量臨時欄位。&lt;/li>
&lt;li>每次 encode 都建立新的 &lt;code>bytes.Buffer&lt;/code>。&lt;/li>
&lt;/ul>
&lt;p>不是所有 allocation 都要消除。診斷重點是找出高頻、可避免、且不破壞邊界的配置。&lt;/p>
&lt;h2 id="判讀預配置解決的是成長成本">【判讀】預配置解決的是成長成本&lt;/h2>
&lt;p>Slice 預配置的核心用途是讓底層 array 成長符合預期。若結果長度可預估，應用 &lt;code>make&lt;/code> 設定容量。&lt;/p>
&lt;p>未預配置：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-go" data-lang="go">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="kd">func&lt;/span> &lt;span class="nf">BuildNames&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">users&lt;/span> &lt;span class="p">[]&lt;/span>&lt;span class="nx">User&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">[]&lt;/span>&lt;span class="kt">string&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl"> &lt;span class="kd">var&lt;/span> &lt;span class="nx">names&lt;/span> &lt;span class="p">[]&lt;/span>&lt;span class="kt">string&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl"> &lt;span class="k">for&lt;/span> &lt;span class="nx">_&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">user&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="k">range&lt;/span> &lt;span class="nx">users&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl"> &lt;span class="nx">names&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="nb">append&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">names&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">user&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">Name&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="nx">names&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>預配置：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-go" data-lang="go">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="kd">func&lt;/span> &lt;span class="nf">BuildNames&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">users&lt;/span> &lt;span class="p">[]&lt;/span>&lt;span class="nx">User&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">[]&lt;/span>&lt;span class="kt">string&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl"> &lt;span class="nx">names&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="nb">make&lt;/span>&lt;span class="p">([]&lt;/span>&lt;span class="kt">string&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">0&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nb">len&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">users&lt;/span>&lt;span class="p">))&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl"> &lt;span class="k">for&lt;/span> &lt;span class="nx">_&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">user&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="k">range&lt;/span> &lt;span class="nx">users&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl"> &lt;span class="nx">names&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="nb">append&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">names&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">user&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">Name&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="nx">names&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>這不是微優化。若這段程式在高頻 list API、background &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/projection/" data-link-title="Projection" data-link-desc="說明從事件流或資料變更推算出查詢用讀取視圖的轉換機制">projection&lt;/a> 或 broadcast path 中執行，預配置可以減少反覆擴容與 copy。&lt;/p>
&lt;h2 id="判讀copy-boundary-是必要成本">【判讀】copy boundary 是必要成本&lt;/h2>
&lt;p>安全複製的核心目的是保護內部可變狀態。Repository 回傳資料時 copy slice 或 map，會增加 allocation，但能避免外部突變與 data race。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-go" data-lang="go">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="kd">func&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">r&lt;/span> &lt;span class="o">*&lt;/span>&lt;span class="nx">UserRepository&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="nf">ListUsers&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">ctx&lt;/span> &lt;span class="nx">context&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">Context&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">([]&lt;/span>&lt;span class="nx">User&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="kt">error&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl"> &lt;span class="nx">r&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">mu&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">RLock&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl"> &lt;span class="k">defer&lt;/span> &lt;span class="nx">r&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">mu&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">RUnlock&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl"> &lt;span class="nx">users&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="nb">make&lt;/span>&lt;span class="p">([]&lt;/span>&lt;span class="nx">User&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">0&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nb">len&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">r&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">users&lt;/span>&lt;span class="p">))&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl"> &lt;span class="k">for&lt;/span> &lt;span class="nx">_&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">user&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="k">range&lt;/span> &lt;span class="nx">r&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">users&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl"> &lt;span class="nx">users&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="nb">append&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">users&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">user&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="nx">users&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="kc">nil&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>這個 allocation 是狀態邊界的成本。優化前要先確認它是否真的是瓶頸，不能只因為 profile 看到配置就移除 copy。&lt;/p>
&lt;p>若列表很大且讀取頻繁，應考慮分頁、projection、snapshot cache 或只回傳必要欄位。不要為了省配置而直接暴露內部 map。&lt;/p>
&lt;h2 id="策略大型-list-優先改資料形狀">【策略】大型 list 優先改資料形狀&lt;/h2>
&lt;p>大型 list allocation 的核心問題常常是 API 一次回太多資料。若每次請求都複製整個 repository，配置與延遲都會隨資料量線性成長。&lt;/p>
&lt;p>可選策略：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>策略&lt;/th>
 &lt;th>適用情境&lt;/th>
 &lt;th>代價&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>分頁&lt;/td>
 &lt;td>使用者只需要部分資料&lt;/td>
 &lt;td>API 需要 cursor 或 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/offset/" data-link-title="Offset" data-link-desc="說明 consumer 在事件流中的讀取位置與重放基準">offset&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>projection&lt;/td>
 &lt;td>只需要摘要欄位&lt;/td>
 &lt;td>要維護讀取模型&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>snapshot cache&lt;/td>
 &lt;td>讀多寫少&lt;/td>
 &lt;td>要處理快取失效&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>incremental update&lt;/td>
 &lt;td>WebSocket 推送最新變化&lt;/td>
 &lt;td>client 要能合併狀態&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>優化資料形狀通常比取消 copy 更安全。Copy boundary 保護正確性，資料形狀決定每次 copy 的成本。&lt;/p></description><content:encoded><![CDATA[<p>Allocation 分析的核心目標是區分必要的安全複製與可優化的重複配置。Go 服務中很多配置來自 slice 成長、map/list 複製、JSON marshal、<a href="/blog/backend/knowledge-cards/buffer/" data-link-title="Buffer" data-link-desc="說明系統如何用暫存空間吸收短暫速度差與尖峰流量">buffer</a> 建立與 <a href="/blog/backend/knowledge-cards/websocket/" data-link-title="WebSocket" data-link-desc="說明 WebSocket 如何提供長連線雙向即時通訊">WebSocket</a> payload；優化前要先確認配置是否位於熱路徑，且不能破壞狀態邊界。</p>
<h2 id="本章目標">本章目標</h2>
<p>學完本章後，你將能夠：</p>
<ol>
<li>理解 allocation 如何增加 GC 壓力</li>
<li>分辨必要 copy boundary 與不必要重複配置</li>
<li>用預配置降低 slice 成長成本</li>
<li>判斷 JSON marshal 與 WebSocket payload 的重用邊界</li>
<li>用 pprof 的 <code>alloc_space</code> 與 <code>inuse_space</code> 決定優化方向</li>
</ol>
<hr>
<h2 id="觀察allocation-壓力會放大-gc-成本">【觀察】allocation 壓力會放大 GC 成本</h2>
<p>Allocation 的核心影響是增加 heap 成長速度，進而增加 GC 工作量。即使物件很快被回收，大量短命配置仍可能造成 CPU 與 latency 壓力。</p>
<p>常見熱路徑：</p>
<ul>
<li>每次 WebSocket broadcast 都對每個 client 重新 marshal。</li>
<li>每次 API list 都建立大型 slice。</li>
<li>每次 repository 查詢都 copy 大型 map。</li>
<li>每次 <a href="/blog/backend/knowledge-cards/log/" data-link-title="Log" data-link-desc="說明 log 如何記錄單一事件的上下文並支援事故排查">log</a> 都組大量臨時欄位。</li>
<li>每次 encode 都建立新的 <code>bytes.Buffer</code>。</li>
</ul>
<p>不是所有 allocation 都要消除。診斷重點是找出高頻、可避免、且不破壞邊界的配置。</p>
<h2 id="判讀預配置解決的是成長成本">【判讀】預配置解決的是成長成本</h2>
<p>Slice 預配置的核心用途是讓底層 array 成長符合預期。若結果長度可預估，應用 <code>make</code> 設定容量。</p>
<p>未預配置：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="ln">1</span><span class="cl"><span class="kd">func</span> <span class="nf">BuildNames</span><span class="p">(</span><span class="nx">users</span> <span class="p">[]</span><span class="nx">User</span><span class="p">)</span> <span class="p">[]</span><span class="kt">string</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">    <span class="kd">var</span> <span class="nx">names</span> <span class="p">[]</span><span class="kt">string</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">    <span class="k">for</span> <span class="nx">_</span><span class="p">,</span> <span class="nx">user</span> <span class="o">:=</span> <span class="k">range</span> <span class="nx">users</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">        <span class="nx">names</span> <span class="p">=</span> <span class="nb">append</span><span class="p">(</span><span class="nx">names</span><span class="p">,</span> <span class="nx">user</span><span class="p">.</span><span class="nx">Name</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">    <span class="k">return</span> <span class="nx">names</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>預配置：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="ln">1</span><span class="cl"><span class="kd">func</span> <span class="nf">BuildNames</span><span class="p">(</span><span class="nx">users</span> <span class="p">[]</span><span class="nx">User</span><span class="p">)</span> <span class="p">[]</span><span class="kt">string</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">    <span class="nx">names</span> <span class="o">:=</span> <span class="nb">make</span><span class="p">([]</span><span class="kt">string</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="nb">len</span><span class="p">(</span><span class="nx">users</span><span class="p">))</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">    <span class="k">for</span> <span class="nx">_</span><span class="p">,</span> <span class="nx">user</span> <span class="o">:=</span> <span class="k">range</span> <span class="nx">users</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">        <span class="nx">names</span> <span class="p">=</span> <span class="nb">append</span><span class="p">(</span><span class="nx">names</span><span class="p">,</span> <span class="nx">user</span><span class="p">.</span><span class="nx">Name</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">    <span class="k">return</span> <span class="nx">names</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>這不是微優化。若這段程式在高頻 list API、background <a href="/blog/backend/knowledge-cards/projection/" data-link-title="Projection" data-link-desc="說明從事件流或資料變更推算出查詢用讀取視圖的轉換機制">projection</a> 或 broadcast path 中執行，預配置可以減少反覆擴容與 copy。</p>
<h2 id="判讀copy-boundary-是必要成本">【判讀】copy boundary 是必要成本</h2>
<p>安全複製的核心目的是保護內部可變狀態。Repository 回傳資料時 copy slice 或 map，會增加 allocation，但能避免外部突變與 data race。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">r</span> <span class="o">*</span><span class="nx">UserRepository</span><span class="p">)</span> <span class="nf">ListUsers</span><span class="p">(</span><span class="nx">ctx</span> <span class="nx">context</span><span class="p">.</span><span class="nx">Context</span><span class="p">)</span> <span class="p">([]</span><span class="nx">User</span><span class="p">,</span> <span class="kt">error</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">    <span class="nx">r</span><span class="p">.</span><span class="nx">mu</span><span class="p">.</span><span class="nf">RLock</span><span class="p">()</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">    <span class="k">defer</span> <span class="nx">r</span><span class="p">.</span><span class="nx">mu</span><span class="p">.</span><span class="nf">RUnlock</span><span class="p">()</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">    <span class="nx">users</span> <span class="o">:=</span> <span class="nb">make</span><span class="p">([]</span><span class="nx">User</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="nb">len</span><span class="p">(</span><span class="nx">r</span><span class="p">.</span><span class="nx">users</span><span class="p">))</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">    <span class="k">for</span> <span class="nx">_</span><span class="p">,</span> <span class="nx">user</span> <span class="o">:=</span> <span class="k">range</span> <span class="nx">r</span><span class="p">.</span><span class="nx">users</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">        <span class="nx">users</span> <span class="p">=</span> <span class="nb">append</span><span class="p">(</span><span class="nx">users</span><span class="p">,</span> <span class="nx">user</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">    <span class="k">return</span> <span class="nx">users</span><span class="p">,</span> <span class="kc">nil</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>這個 allocation 是狀態邊界的成本。優化前要先確認它是否真的是瓶頸，不能只因為 profile 看到配置就移除 copy。</p>
<p>若列表很大且讀取頻繁，應考慮分頁、projection、snapshot cache 或只回傳必要欄位。不要為了省配置而直接暴露內部 map。</p>
<h2 id="策略大型-list-優先改資料形狀">【策略】大型 list 優先改資料形狀</h2>
<p>大型 list allocation 的核心問題常常是 API 一次回太多資料。若每次請求都複製整個 repository，配置與延遲都會隨資料量線性成長。</p>
<p>可選策略：</p>
<table>
  <thead>
      <tr>
          <th>策略</th>
          <th>適用情境</th>
          <th>代價</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>分頁</td>
          <td>使用者只需要部分資料</td>
          <td>API 需要 cursor 或 <a href="/blog/backend/knowledge-cards/offset/" data-link-title="Offset" data-link-desc="說明 consumer 在事件流中的讀取位置與重放基準">offset</a></td>
      </tr>
      <tr>
          <td>projection</td>
          <td>只需要摘要欄位</td>
          <td>要維護讀取模型</td>
      </tr>
      <tr>
          <td>snapshot cache</td>
          <td>讀多寫少</td>
          <td>要處理快取失效</td>
      </tr>
      <tr>
          <td>incremental update</td>
          <td>WebSocket 推送最新變化</td>
          <td>client 要能合併狀態</td>
      </tr>
  </tbody>
</table>
<p>優化資料形狀通常比取消 copy 更安全。Copy boundary 保護正確性，資料形狀決定每次 copy 的成本。</p>
<h2 id="執行json-marshal-是-websocket-常見配置來源">【執行】JSON marshal 是 WebSocket 常見配置來源</h2>
<p>JSON 序列化的核心成本是把 Go 資料結構轉成 bytes。高頻 WebSocket 推送若對每個 client 反覆 marshal 同一份 message，會造成大量短命配置。</p>
<p>反模式：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">for</span> <span class="nx">_</span><span class="p">,</span> <span class="nx">client</span> <span class="o">:=</span> <span class="k">range</span> <span class="nx">clients</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">    <span class="nx">payload</span><span class="p">,</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">json</span><span class="p">.</span><span class="nf">Marshal</span><span class="p">(</span><span class="nx">message</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">    <span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">        <span class="k">return</span> <span class="nx">err</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">    <span class="nx">client</span><span class="p">.</span><span class="nf">SendBytes</span><span class="p">(</span><span class="nx">payload</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>同一份 message 可以先 marshal 一次：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="ln">1</span><span class="cl"><span class="nx">payload</span><span class="p">,</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">json</span><span class="p">.</span><span class="nf">Marshal</span><span class="p">(</span><span class="nx">message</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">    <span class="k">return</span> <span class="nx">err</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="k">for</span> <span class="nx">_</span><span class="p">,</span> <span class="nx">client</span> <span class="o">:=</span> <span class="k">range</span> <span class="nx">clients</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl">    <span class="nx">client</span><span class="p">.</span><span class="nf">SendBytes</span><span class="p">(</span><span class="nx">payload</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>這個優化的前提是 <code>payload</code> 被視為只讀。Send path 不應修改傳入的 bytes；若某個 client 需要修改，就應在該 client 邊界 copy，而不是讓共享 payload 被改動。</p>
<h2 id="判讀bytes-重用要先定義所有權">【判讀】bytes 重用要先定義所有權</h2>
<p>Bytes 重用的核心風險是共享 slice 被修改。<code>[]byte</code> 是可變資料，傳給多個 client 時要明確規定它只讀。</p>
<p>可以用型別或註解表達語意：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="ln">1</span><span class="cl"><span class="kd">type</span> <span class="nx">EncodedMessage</span> <span class="p">[]</span><span class="kt">byte</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">c</span> <span class="o">*</span><span class="nx">Client</span><span class="p">)</span> <span class="nf">SendEncoded</span><span class="p">(</span><span class="nx">message</span> <span class="nx">EncodedMessage</span><span class="p">)</span> <span class="kt">bool</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">    <span class="k">return</span> <span class="nx">c</span><span class="p">.</span><span class="nf">TrySend</span><span class="p">(</span><span class="nx">ServerMessage</span><span class="p">{</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">        <span class="nx">Encoded</span><span class="p">:</span> <span class="nx">message</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">    <span class="p">})</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>這不能從型別上完全禁止修改，但能讓 API 語意更清楚。真正保護仍靠 ownership 規則、測試與 code review。</p>
<p>若無法保證下游不修改，就必須 copy：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="ln">1</span><span class="cl"><span class="kd">func</span> <span class="nf">CloneBytes</span><span class="p">(</span><span class="nx">input</span> <span class="p">[]</span><span class="kt">byte</span><span class="p">)</span> <span class="p">[]</span><span class="kt">byte</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">    <span class="nx">output</span> <span class="o">:=</span> <span class="nb">make</span><span class="p">([]</span><span class="kt">byte</span><span class="p">,</span> <span class="nb">len</span><span class="p">(</span><span class="nx">input</span><span class="p">))</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">    <span class="nb">copy</span><span class="p">(</span><span class="nx">output</span><span class="p">,</span> <span class="nx">input</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">    <span class="k">return</span> <span class="nx">output</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>效能優化不能建立在模糊的可變資料共享上。</p>
<h2 id="策略syncpool-只適合已證明的熱路徑">【策略】sync.Pool 只適合已證明的熱路徑</h2>
<p><code>sync.Pool</code> 的核心用途是複用高頻、短命、可重建的暫存物件。它可以降低配置，但會增加所有權複雜度。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="kd">var</span> <span class="nx">bufferPool</span> <span class="p">=</span> <span class="nx">sync</span><span class="p">.</span><span class="nx">Pool</span><span class="p">{</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">    <span class="nx">New</span><span class="p">:</span> <span class="kd">func</span><span class="p">()</span> <span class="kt">any</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">        <span class="k">return</span> <span class="nb">new</span><span class="p">(</span><span class="nx">bytes</span><span class="p">.</span><span class="nx">Buffer</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">    <span class="p">},</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="kd">func</span> <span class="nf">Encode</span><span class="p">(</span><span class="nx">value</span> <span class="kt">any</span><span class="p">)</span> <span class="p">([]</span><span class="kt">byte</span><span class="p">,</span> <span class="kt">error</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">    <span class="nx">buf</span> <span class="o">:=</span> <span class="nx">bufferPool</span><span class="p">.</span><span class="nf">Get</span><span class="p">().(</span><span class="o">*</span><span class="nx">bytes</span><span class="p">.</span><span class="nx">Buffer</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">    <span class="k">defer</span> <span class="nx">bufferPool</span><span class="p">.</span><span class="nf">Put</span><span class="p">(</span><span class="nx">buf</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">    <span class="nx">buf</span><span class="p">.</span><span class="nf">Reset</span><span class="p">()</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">
</span></span><span class="line"><span class="ln">12</span><span class="cl">    <span class="k">if</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">json</span><span class="p">.</span><span class="nf">NewEncoder</span><span class="p">(</span><span class="nx">buf</span><span class="p">).</span><span class="nf">Encode</span><span class="p">(</span><span class="nx">value</span><span class="p">);</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">        <span class="k">return</span> <span class="kc">nil</span><span class="p">,</span> <span class="nx">err</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">
</span></span><span class="line"><span class="ln">16</span><span class="cl">    <span class="nx">output</span> <span class="o">:=</span> <span class="nb">append</span><span class="p">([]</span><span class="nb">byte</span><span class="p">(</span><span class="kc">nil</span><span class="p">),</span> <span class="nx">buf</span><span class="p">.</span><span class="nf">Bytes</span><span class="p">()</span><span class="o">...</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl">    <span class="k">return</span> <span class="nx">output</span><span class="p">,</span> <span class="kc">nil</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>這裡仍然 copy 出 <code>output</code>，因為 <code>buf</code> 會被放回 pool。若直接回傳 <code>buf.Bytes()</code>，呼叫端拿到的 slice 可能在 pool 重用後被覆寫。</p>
<p>不要一開始就使用 <code>sync.Pool</code>。先用 pprof 證明配置是瓶頸，再評估 pool 是否值得承擔額外複雜度。</p>
<h2 id="判讀inuse-與-alloc-回答不同問題">【判讀】inuse 與 alloc 回答不同問題</h2>
<p>Heap profile 的核心判讀是分清 <code>inuse_space</code> 與 <code>alloc_space</code>。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">go tool pprof http://localhost:8080/debug/pprof/heap
</span></span><span class="line"><span class="ln">2</span><span class="cl">go tool pprof -alloc_space http://localhost:8080/debug/pprof/heap</span></span></code></pre></div><table>
  <thead>
      <tr>
          <th>指標</th>
          <th>問題</th>
          <th>常見修正</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>inuse_space</code> 高</td>
          <td>現在誰保留記憶體</td>
          <td>cache 淘汰、釋放引用、限制 buffer</td>
      </tr>
      <tr>
          <td><code>alloc_space</code> 高</td>
          <td>誰累積配置最多</td>
          <td>預配置、重用、減少 marshal、改資料形狀</td>
      </tr>
  </tbody>
</table>
<p>若 <code>alloc_space</code> 高但 <code>inuse_space</code> 不高，代表配置很多但大多被回收，問題可能是 GC 壓力。若 <code>inuse_space</code> 持續上升，代表資料被長期保留，應檢查 cache、map、slice、goroutine reference 或 send buffer。</p>
<h2 id="策略allocation-優化要保留正確性邊界">【策略】allocation 優化要保留正確性邊界</h2>
<p>Allocation 優化的核心底線是不能破壞狀態安全。以下做法通常不可接受：</p>
<ul>
<li>為了省 copy，直接回傳 repository 內部 map。</li>
<li>為了省 bytes，讓多個 client 共享可修改 payload。</li>
<li>為了省 allocation，把 buffer 放回 pool 後仍回傳其底層 slice。</li>
<li>為了少建立 struct，把 request DTO 和 domain state 混用。</li>
</ul>
<p>較安全的優化順序：</p>
<ol>
<li>用 pprof 確認熱點。</li>
<li>預配置已知大小的 slice/map。</li>
<li>減少重複 marshal。</li>
<li>改 API 資料形狀，例如分頁或 projection。</li>
<li>最後才考慮 <code>sync.Pool</code>。</li>
</ol>
<p>這個順序先處理低風險、高可讀性的改動，再處理高複雜度工具。</p>
<h2 id="測試優化後要補邊界測試">【測試】優化後要補邊界測試</h2>
<p>Allocation 優化的測試核心是確保共享資料沒有被外部修改。若你重用 bytes、snapshot 或 pooled buffer，要補測試保護 ownership。</p>
<p>例如 repository list 仍要回傳 copy：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="kd">func</span> <span class="nf">TestListUsersReturnsCopy</span><span class="p">(</span><span class="nx">t</span> <span class="o">*</span><span class="nx">testing</span><span class="p">.</span><span class="nx">T</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">    <span class="nx">repo</span> <span class="o">:=</span> <span class="nf">NewUserRepository</span><span class="p">()</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">    <span class="nx">ctx</span> <span class="o">:=</span> <span class="nx">context</span><span class="p">.</span><span class="nf">Background</span><span class="p">()</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">    <span class="nx">_</span> <span class="p">=</span> <span class="nx">repo</span><span class="p">.</span><span class="nf">Save</span><span class="p">(</span><span class="nx">ctx</span><span class="p">,</span> <span class="nx">User</span><span class="p">{</span><span class="nx">ID</span><span class="p">:</span> <span class="s">&#34;user_1&#34;</span><span class="p">})</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">    <span class="nx">users</span><span class="p">,</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">repo</span><span class="p">.</span><span class="nf">ListUsers</span><span class="p">(</span><span class="nx">ctx</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">    <span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">        <span class="nx">t</span><span class="p">.</span><span class="nf">Fatalf</span><span class="p">(</span><span class="s">&#34;list users: %v&#34;</span><span class="p">,</span> <span class="nx">err</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">    <span class="nx">users</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="nx">ID</span> <span class="p">=</span> <span class="s">&#34;changed&#34;</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">
</span></span><span class="line"><span class="ln">12</span><span class="cl">    <span class="nx">again</span><span class="p">,</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">repo</span><span class="p">.</span><span class="nf">ListUsers</span><span class="p">(</span><span class="nx">ctx</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">    <span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">        <span class="nx">t</span><span class="p">.</span><span class="nf">Fatalf</span><span class="p">(</span><span class="s">&#34;list users again: %v&#34;</span><span class="p">,</span> <span class="nx">err</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">    <span class="k">if</span> <span class="nx">again</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="nx">ID</span> <span class="o">!=</span> <span class="s">&#34;user_1&#34;</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl">        <span class="nx">t</span><span class="p">.</span><span class="nf">Fatalf</span><span class="p">(</span><span class="s">&#34;repository data was modified through returned slice&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln">19</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>這種測試能防止未來為了省 allocation 而移除必要 copy。</p>
<h2 id="本章不處理">本章不處理</h2>
<p>本章先處理熱路徑上的配置與資料形狀；更大範圍的序列化與 payload 策略，會在下列章節再往外延伸：</p>
<ul>
<li><a href="/blog/go/02-types-data/struct-json/" data-link-title="2.1 struct 與 JSON tag" data-link-desc="理解 Go struct 如何表達資料形狀，並透過 JSON tag 對應外部格式">Go 入門：struct 與 JSON tag</a></li>
<li><a href="/blog/go/02-types-data/slices-maps/" data-link-title="2.2 slice 與 map" data-link-desc="掌握 Go 最常用的集合型別：slice 與 map">Go 入門：slice 與 map</a></li>
<li><a href="/blog/go/02-types-data/pointers-copy/" data-link-title="2.5 指標與資料複製邊界" data-link-desc="理解指標、slice 與共享狀態的防護策略">Go 入門：指標與資料複製邊界</a></li>
<li><a href="/blog/go-advanced/03-runtime-profiling/pprof/" data-link-title="3.2 pprof 基礎診斷流程" data-link-desc="用 pprof endpoint 診斷 heap、goroutine 與 CPU 問題">Go 進階：pprof 基礎診斷流程</a></li>
</ul>
<h2 id="和-go-教材的關係">和 Go 教材的關係</h2>
<p>這一章承接的是 copy boundary、JSON 與 runtime profile；如果你要先回看語言教材，可以讀：</p>
<ul>
<li><a href="/blog/go/06-practical/state-fields/" data-link-title="6.3 如何擴展狀態投影欄位" data-link-desc="更新狀態模型、repository 與 API 輸出">Go：如何擴展狀態投影欄位</a></li>
<li><a href="/blog/go/06-practical/repository-port/" data-link-title="6.6 如何新增 repository port" data-link-desc="先建立儲存邊界，再決定 memory、SQLite 或外部資料庫實作">Go：如何新增 repository port</a></li>
<li><a href="/blog/go/06-practical/new-websocket-action/" data-link-title="6.1 如何新增一個即時訊息 action" data-link-desc="修改 client message、路由與 handler">Go：如何新增一個即時訊息 action</a></li>
<li><a href="/blog/go/07-refactoring/state-boundary/" data-link-title="7.4 狀態管理的安全邊界" data-link-desc="用 lock、copy 與 API 限制保護共享狀態">Go：狀態管理的安全邊界</a></li>
</ul>
<h2 id="小結">小結</h2>
<p>Allocation 優化要先判斷配置是否必要。保護狀態的 copy 是合理成本，高頻熱路徑的重複配置才是優先目標。JSON marshal、slice 成長、map/list 複製與 buffer 建立都是常見來源；用 pprof 區分 <code>inuse_space</code> 與 <code>alloc_space</code> 後，再決定預配置、分頁、projection、payload 重用或 <code>sync.Pool</code>。</p>
]]></content:encoded></item><item><title>2.5 指標與資料複製邊界</title><link>https://tarrragon.github.io/blog/go/02-types-data/pointers-copy/</link><pubDate>Wed, 22 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/go/02-types-data/pointers-copy/</guid><description>&lt;p>Go 的指標讓函式可以操作原本的資料，而不是資料複本。這很有效率，也很危險：當多個地方共享同一份資料時，你需要明確決定誰可以修改，誰只能讀取。&lt;/p>
&lt;h2 id="本章目標">本章目標&lt;/h2>
&lt;p>學完本章後，你將能夠：&lt;/p>
&lt;ol>
&lt;li>理解值傳遞與指標傳遞的差異&lt;/li>
&lt;li>判斷何時使用 pointer&lt;/li>
&lt;li>理解 slice、map 本身已經帶有共享底層資料的特性&lt;/li>
&lt;li>用 copy 保護資料邊界&lt;/li>
&lt;/ol>
&lt;hr>
&lt;h2 id="觀察go-預設是值傳遞">【觀察】Go 預設是值傳遞&lt;/h2>
&lt;p>值傳遞的核心規則是：函式收到的是參數值的複本，修改複本不會改到呼叫端原值。以下範例中，&lt;code>Rename&lt;/code> 修改的是複本：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-go" data-lang="go">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="kd">type&lt;/span> &lt;span class="nx">User&lt;/span> &lt;span class="kd">struct&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl"> &lt;span class="nx">Name&lt;/span> &lt;span class="kt">string&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl">&lt;span class="kd">func&lt;/span> &lt;span class="nf">Rename&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">u&lt;/span> &lt;span class="nx">User&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl"> &lt;span class="nx">u&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">Name&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s">&amp;#34;Bob&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">&lt;span class="kd">func&lt;/span> &lt;span class="nf">main&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl"> &lt;span class="nx">user&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="nx">User&lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="nx">Name&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s">&amp;#34;Alice&amp;#34;&lt;/span>&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl"> &lt;span class="nf">Rename&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">user&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl"> &lt;span class="nx">fmt&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Println&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">user&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">Name&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="c1">// Alice&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">13&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>Rename&lt;/code> 修改的是複本，不是 &lt;code>main&lt;/code> 裡的 &lt;code>user&lt;/code>。&lt;/p>
&lt;p>指標傳遞的核心規則是：函式收到原值位址，因此可以修改呼叫端原值。如果想修改原本的值，就要傳指標：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-go" data-lang="go">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="kd">func&lt;/span> &lt;span class="nf">Rename&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">u&lt;/span> &lt;span class="o">*&lt;/span>&lt;span class="nx">User&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl"> &lt;span class="nx">u&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">Name&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s">&amp;#34;Bob&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="kd">func&lt;/span> &lt;span class="nf">main&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl"> &lt;span class="nx">user&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="nx">User&lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="nx">Name&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s">&amp;#34;Alice&amp;#34;&lt;/span>&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl"> &lt;span class="nf">Rename&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="o">&amp;amp;&lt;/span>&lt;span class="nx">user&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">8&lt;/span>&lt;span class="cl"> &lt;span class="nx">fmt&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Println&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">user&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">Name&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="c1">// Bob&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">9&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>&amp;amp;user&lt;/code> 取得位址，&lt;code>*User&lt;/code> 表示指向 &lt;code>User&lt;/code> 的指標。&lt;/p>
&lt;h2 id="判讀pointer-表示共享修改權">【判讀】pointer 表示共享修改權&lt;/h2>
&lt;p>pointer 的核心語意是共享修改權，不只是效能工具。它表示被呼叫者可能看到或修改原本那份資料。&lt;/p>
&lt;p>適合使用 pointer 的情境：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>情境&lt;/th>
 &lt;th>原因&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>方法需要修改 receiver&lt;/td>
 &lt;td>例如 &lt;code>Counter.Inc()&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>struct 很大，複製成本高&lt;/td>
 &lt;td>避免每次呼叫都複製大量資料&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>需要表示 optional object&lt;/td>
 &lt;td>&lt;code>nil&lt;/code> 可表示不存在&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>多個方法共享同一份狀態&lt;/td>
 &lt;td>例如 repository、server、cache&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>不適合濫用 pointer 的情境：&lt;/p>
&lt;ul>
&lt;li>小型不可變資料，例如 &lt;code>time.Time&lt;/code> 常直接值傳遞&lt;/li>
&lt;li>只是為了「看起來像物件導向」&lt;/li>
&lt;li>不希望呼叫者能修改內部資料&lt;/li>
&lt;/ul>
&lt;h2 id="策略slice-和-map-要特別小心">【策略】slice 和 map 要特別小心&lt;/h2>
&lt;p>slice 和 map 的核心風險是：即使參數不是 pointer，也會共享底層資料。&lt;/p>
&lt;h3 id="slice-共享底層陣列">slice 共享底層陣列&lt;/h3>
&lt;p>slice 參數會複製 slice header，但 header 指向同一個底層 array；因此函式內修改元素，外面會看見。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-go" data-lang="go">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="kd">func&lt;/span> &lt;span class="nf">Modify&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">items&lt;/span> &lt;span class="p">[]&lt;/span>&lt;span class="kt">string&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl"> &lt;span class="nx">items&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s">&amp;#34;changed&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="kd">func&lt;/span> &lt;span class="nf">main&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl"> &lt;span class="nx">names&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="p">[]&lt;/span>&lt;span class="kt">string&lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="s">&amp;#34;Alice&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s">&amp;#34;Bob&amp;#34;&lt;/span>&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl"> &lt;span class="nf">Modify&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">names&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">8&lt;/span>&lt;span class="cl"> &lt;span class="nx">fmt&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Println&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">names&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">])&lt;/span> &lt;span class="c1">// changed&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">9&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="map-本身就是-reference-like">map 本身就是 reference-like&lt;/h3>
&lt;p>map 傳入函式後，函式可以修改同一份 map。這是很多共享狀態 bug 的來源。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-go" data-lang="go">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="kd">func&lt;/span> &lt;span class="nf">Modify&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">m&lt;/span> &lt;span class="kd">map&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="kt">string&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="kt">int&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl"> &lt;span class="nx">m&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s">&amp;#34;count&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="mi">10&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="kd">func&lt;/span> &lt;span class="nf">main&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl"> &lt;span class="nx">values&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="kd">map&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="kt">string&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="kt">int&lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="s">&amp;#34;count&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">1&lt;/span>&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl"> &lt;span class="nf">Modify&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">values&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">8&lt;/span>&lt;span class="cl"> &lt;span class="nx">fmt&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Println&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">values&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s">&amp;#34;count&amp;#34;&lt;/span>&lt;span class="p">])&lt;/span> &lt;span class="c1">// 10&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">9&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="執行回傳資料時建立-copy-邊界">【執行】回傳資料時建立 copy 邊界&lt;/h2>
&lt;p>copy 邊界的核心規則是：不希望外部修改內部狀態時，不要直接回傳內部 map、slice 或 pointer。假設 &lt;code>UserRepository&lt;/code> 內部保存一組使用者：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-go" data-lang="go">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="kd">type&lt;/span> &lt;span class="nx">User&lt;/span> &lt;span class="kd">struct&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl"> &lt;span class="nx">ID&lt;/span> &lt;span class="kt">string&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl"> &lt;span class="nx">Name&lt;/span> &lt;span class="kt">string&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">&lt;span class="kd">type&lt;/span> &lt;span class="nx">UserRepository&lt;/span> &lt;span class="kd">struct&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl"> &lt;span class="nx">users&lt;/span> &lt;span class="kd">map&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="kt">string&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="nx">User&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">8&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>直接回傳 map 會把內部狀態暴露給呼叫者：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-go" data-lang="go">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="kd">func&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">r&lt;/span> &lt;span class="o">*&lt;/span>&lt;span class="nx">UserRepository&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="nf">Users&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="kd">map&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="kt">string&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="nx">User&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="nx">r&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">users&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>呼叫者就可以繞過 &lt;code>UserRepository&lt;/code> 修改內部資料：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-go" data-lang="go">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="nx">users&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="nx">repo&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Users&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="nx">users&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s">&amp;#34;1&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="nx">User&lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="nx">ID&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s">&amp;#34;1&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">Name&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s">&amp;#34;Changed&amp;#34;&lt;/span>&lt;span class="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>安全做法是回傳複製：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-go" data-lang="go">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="kd">func&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">r&lt;/span> &lt;span class="o">*&lt;/span>&lt;span class="nx">UserRepository&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="nf">Users&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="kd">map&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="kt">string&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="nx">User&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl"> &lt;span class="nx">result&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="nb">make&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="kd">map&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="kt">string&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="nx">User&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nb">len&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">r&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">users&lt;/span>&lt;span class="p">))&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl"> &lt;span class="k">for&lt;/span> &lt;span class="nx">id&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">user&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="k">range&lt;/span> &lt;span class="nx">r&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">users&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl"> &lt;span class="nx">result&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="nx">id&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="nx">user&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="nx">result&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>回傳 slice 時也要複製 slice：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-go" data-lang="go">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="kd">func&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">r&lt;/span> &lt;span class="o">*&lt;/span>&lt;span class="nx">UserRepository&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="nf">ListUsers&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="p">[]&lt;/span>&lt;span class="nx">User&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl"> &lt;span class="nx">result&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="nb">make&lt;/span>&lt;span class="p">([]&lt;/span>&lt;span class="nx">User&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">0&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nb">len&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">r&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">users&lt;/span>&lt;span class="p">))&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl"> &lt;span class="k">for&lt;/span> &lt;span class="nx">_&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">user&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="k">range&lt;/span> &lt;span class="nx">r&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">users&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl"> &lt;span class="nx">result&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="nb">append&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">result&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">user&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="nx">result&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>這樣呼叫者可以自由排序、append 或修改回傳資料，不會影響 repository 內部狀態。&lt;/p>
&lt;h2 id="深層複製與淺層複製">深層複製與淺層複製&lt;/h2>
&lt;p>深層複製的核心規則是：struct 裡面若含 slice、map 或 pointer，只複製 struct 本身仍會共享內部資料。以下 &lt;code>Profile&lt;/code> 包含 slice：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-go" data-lang="go">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="kd">type&lt;/span> &lt;span class="nx">Profile&lt;/span> &lt;span class="kd">struct&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl"> &lt;span class="nx">Name&lt;/span> &lt;span class="kt">string&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl"> &lt;span class="nx">Tags&lt;/span> &lt;span class="p">[]&lt;/span>&lt;span class="kt">string&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>淺層複製：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-go" data-lang="go">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="nx">copyProfile&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="nx">profile&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>copyProfile.Tags&lt;/code> 和 &lt;code>profile.Tags&lt;/code> 仍然指向同一個底層 array。若要保護邊界，需要複製 slice：&lt;/p></description><content:encoded><![CDATA[<p>Go 的指標讓函式可以操作原本的資料，而不是資料複本。這很有效率，也很危險：當多個地方共享同一份資料時，你需要明確決定誰可以修改，誰只能讀取。</p>
<h2 id="本章目標">本章目標</h2>
<p>學完本章後，你將能夠：</p>
<ol>
<li>理解值傳遞與指標傳遞的差異</li>
<li>判斷何時使用 pointer</li>
<li>理解 slice、map 本身已經帶有共享底層資料的特性</li>
<li>用 copy 保護資料邊界</li>
</ol>
<hr>
<h2 id="觀察go-預設是值傳遞">【觀察】Go 預設是值傳遞</h2>
<p>值傳遞的核心規則是：函式收到的是參數值的複本，修改複本不會改到呼叫端原值。以下範例中，<code>Rename</code> 修改的是複本：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="kd">type</span> <span class="nx">User</span> <span class="kd">struct</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">    <span class="nx">Name</span> <span class="kt">string</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">
</span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="kd">func</span> <span class="nf">Rename</span><span class="p">(</span><span class="nx">u</span> <span class="nx">User</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">    <span class="nx">u</span><span class="p">.</span><span class="nx">Name</span> <span class="p">=</span> <span class="s">&#34;Bob&#34;</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="kd">func</span> <span class="nf">main</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">    <span class="nx">user</span> <span class="o">:=</span> <span class="nx">User</span><span class="p">{</span><span class="nx">Name</span><span class="p">:</span> <span class="s">&#34;Alice&#34;</span><span class="p">}</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">    <span class="nf">Rename</span><span class="p">(</span><span class="nx">user</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">    <span class="nx">fmt</span><span class="p">.</span><span class="nf">Println</span><span class="p">(</span><span class="nx">user</span><span class="p">.</span><span class="nx">Name</span><span class="p">)</span> <span class="c1">// Alice</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p><code>Rename</code> 修改的是複本，不是 <code>main</code> 裡的 <code>user</code>。</p>
<p>指標傳遞的核心規則是：函式收到原值位址，因此可以修改呼叫端原值。如果想修改原本的值，就要傳指標：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="ln">1</span><span class="cl"><span class="kd">func</span> <span class="nf">Rename</span><span class="p">(</span><span class="nx">u</span> <span class="o">*</span><span class="nx">User</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">    <span class="nx">u</span><span class="p">.</span><span class="nx">Name</span> <span class="p">=</span> <span class="s">&#34;Bob&#34;</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="kd">func</span> <span class="nf">main</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">    <span class="nx">user</span> <span class="o">:=</span> <span class="nx">User</span><span class="p">{</span><span class="nx">Name</span><span class="p">:</span> <span class="s">&#34;Alice&#34;</span><span class="p">}</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl">    <span class="nf">Rename</span><span class="p">(</span><span class="o">&amp;</span><span class="nx">user</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl">    <span class="nx">fmt</span><span class="p">.</span><span class="nf">Println</span><span class="p">(</span><span class="nx">user</span><span class="p">.</span><span class="nx">Name</span><span class="p">)</span> <span class="c1">// Bob</span>
</span></span><span class="line"><span class="ln">9</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p><code>&amp;user</code> 取得位址，<code>*User</code> 表示指向 <code>User</code> 的指標。</p>
<h2 id="判讀pointer-表示共享修改權">【判讀】pointer 表示共享修改權</h2>
<p>pointer 的核心語意是共享修改權，不只是效能工具。它表示被呼叫者可能看到或修改原本那份資料。</p>
<p>適合使用 pointer 的情境：</p>
<table>
  <thead>
      <tr>
          <th>情境</th>
          <th>原因</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>方法需要修改 receiver</td>
          <td>例如 <code>Counter.Inc()</code></td>
      </tr>
      <tr>
          <td>struct 很大，複製成本高</td>
          <td>避免每次呼叫都複製大量資料</td>
      </tr>
      <tr>
          <td>需要表示 optional object</td>
          <td><code>nil</code> 可表示不存在</td>
      </tr>
      <tr>
          <td>多個方法共享同一份狀態</td>
          <td>例如 repository、server、cache</td>
      </tr>
  </tbody>
</table>
<p>不適合濫用 pointer 的情境：</p>
<ul>
<li>小型不可變資料，例如 <code>time.Time</code> 常直接值傳遞</li>
<li>只是為了「看起來像物件導向」</li>
<li>不希望呼叫者能修改內部資料</li>
</ul>
<h2 id="策略slice-和-map-要特別小心">【策略】slice 和 map 要特別小心</h2>
<p>slice 和 map 的核心風險是：即使參數不是 pointer，也會共享底層資料。</p>
<h3 id="slice-共享底層陣列">slice 共享底層陣列</h3>
<p>slice 參數會複製 slice header，但 header 指向同一個底層 array；因此函式內修改元素，外面會看見。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="ln">1</span><span class="cl"><span class="kd">func</span> <span class="nf">Modify</span><span class="p">(</span><span class="nx">items</span> <span class="p">[]</span><span class="kt">string</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">    <span class="nx">items</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="p">=</span> <span class="s">&#34;changed&#34;</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="kd">func</span> <span class="nf">main</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">    <span class="nx">names</span> <span class="o">:=</span> <span class="p">[]</span><span class="kt">string</span><span class="p">{</span><span class="s">&#34;Alice&#34;</span><span class="p">,</span> <span class="s">&#34;Bob&#34;</span><span class="p">}</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl">    <span class="nf">Modify</span><span class="p">(</span><span class="nx">names</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl">    <span class="nx">fmt</span><span class="p">.</span><span class="nf">Println</span><span class="p">(</span><span class="nx">names</span><span class="p">[</span><span class="mi">0</span><span class="p">])</span> <span class="c1">// changed</span>
</span></span><span class="line"><span class="ln">9</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><h3 id="map-本身就是-reference-like">map 本身就是 reference-like</h3>
<p>map 傳入函式後，函式可以修改同一份 map。這是很多共享狀態 bug 的來源。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="ln">1</span><span class="cl"><span class="kd">func</span> <span class="nf">Modify</span><span class="p">(</span><span class="nx">m</span> <span class="kd">map</span><span class="p">[</span><span class="kt">string</span><span class="p">]</span><span class="kt">int</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">    <span class="nx">m</span><span class="p">[</span><span class="s">&#34;count&#34;</span><span class="p">]</span> <span class="p">=</span> <span class="mi">10</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="kd">func</span> <span class="nf">main</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">    <span class="nx">values</span> <span class="o">:=</span> <span class="kd">map</span><span class="p">[</span><span class="kt">string</span><span class="p">]</span><span class="kt">int</span><span class="p">{</span><span class="s">&#34;count&#34;</span><span class="p">:</span> <span class="mi">1</span><span class="p">}</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl">    <span class="nf">Modify</span><span class="p">(</span><span class="nx">values</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl">    <span class="nx">fmt</span><span class="p">.</span><span class="nf">Println</span><span class="p">(</span><span class="nx">values</span><span class="p">[</span><span class="s">&#34;count&#34;</span><span class="p">])</span> <span class="c1">// 10</span>
</span></span><span class="line"><span class="ln">9</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><h2 id="執行回傳資料時建立-copy-邊界">【執行】回傳資料時建立 copy 邊界</h2>
<p>copy 邊界的核心規則是：不希望外部修改內部狀態時，不要直接回傳內部 map、slice 或 pointer。假設 <code>UserRepository</code> 內部保存一組使用者：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="ln">1</span><span class="cl"><span class="kd">type</span> <span class="nx">User</span> <span class="kd">struct</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">    <span class="nx">ID</span>   <span class="kt">string</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">    <span class="nx">Name</span> <span class="kt">string</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="kd">type</span> <span class="nx">UserRepository</span> <span class="kd">struct</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl">    <span class="nx">users</span> <span class="kd">map</span><span class="p">[</span><span class="kt">string</span><span class="p">]</span><span class="nx">User</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>直接回傳 map 會把內部狀態暴露給呼叫者：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="ln">1</span><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">r</span> <span class="o">*</span><span class="nx">UserRepository</span><span class="p">)</span> <span class="nf">Users</span><span class="p">()</span> <span class="kd">map</span><span class="p">[</span><span class="kt">string</span><span class="p">]</span><span class="nx">User</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">    <span class="k">return</span> <span class="nx">r</span><span class="p">.</span><span class="nx">users</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>呼叫者就可以繞過 <code>UserRepository</code> 修改內部資料：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="ln">1</span><span class="cl"><span class="nx">users</span> <span class="o">:=</span> <span class="nx">repo</span><span class="p">.</span><span class="nf">Users</span><span class="p">()</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="nx">users</span><span class="p">[</span><span class="s">&#34;1&#34;</span><span class="p">]</span> <span class="p">=</span> <span class="nx">User</span><span class="p">{</span><span class="nx">ID</span><span class="p">:</span> <span class="s">&#34;1&#34;</span><span class="p">,</span> <span class="nx">Name</span><span class="p">:</span> <span class="s">&#34;Changed&#34;</span><span class="p">}</span></span></span></code></pre></div><p>安全做法是回傳複製：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="ln">1</span><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">r</span> <span class="o">*</span><span class="nx">UserRepository</span><span class="p">)</span> <span class="nf">Users</span><span class="p">()</span> <span class="kd">map</span><span class="p">[</span><span class="kt">string</span><span class="p">]</span><span class="nx">User</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">    <span class="nx">result</span> <span class="o">:=</span> <span class="nb">make</span><span class="p">(</span><span class="kd">map</span><span class="p">[</span><span class="kt">string</span><span class="p">]</span><span class="nx">User</span><span class="p">,</span> <span class="nb">len</span><span class="p">(</span><span class="nx">r</span><span class="p">.</span><span class="nx">users</span><span class="p">))</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">    <span class="k">for</span> <span class="nx">id</span><span class="p">,</span> <span class="nx">user</span> <span class="o">:=</span> <span class="k">range</span> <span class="nx">r</span><span class="p">.</span><span class="nx">users</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">        <span class="nx">result</span><span class="p">[</span><span class="nx">id</span><span class="p">]</span> <span class="p">=</span> <span class="nx">user</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">    <span class="k">return</span> <span class="nx">result</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>回傳 slice 時也要複製 slice：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="ln">1</span><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">r</span> <span class="o">*</span><span class="nx">UserRepository</span><span class="p">)</span> <span class="nf">ListUsers</span><span class="p">()</span> <span class="p">[]</span><span class="nx">User</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">    <span class="nx">result</span> <span class="o">:=</span> <span class="nb">make</span><span class="p">([]</span><span class="nx">User</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="nb">len</span><span class="p">(</span><span class="nx">r</span><span class="p">.</span><span class="nx">users</span><span class="p">))</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">    <span class="k">for</span> <span class="nx">_</span><span class="p">,</span> <span class="nx">user</span> <span class="o">:=</span> <span class="k">range</span> <span class="nx">r</span><span class="p">.</span><span class="nx">users</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">        <span class="nx">result</span> <span class="p">=</span> <span class="nb">append</span><span class="p">(</span><span class="nx">result</span><span class="p">,</span> <span class="nx">user</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">    <span class="k">return</span> <span class="nx">result</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>這樣呼叫者可以自由排序、append 或修改回傳資料，不會影響 repository 內部狀態。</p>
<h2 id="深層複製與淺層複製">深層複製與淺層複製</h2>
<p>深層複製的核心規則是：struct 裡面若含 slice、map 或 pointer，只複製 struct 本身仍會共享內部資料。以下 <code>Profile</code> 包含 slice：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="ln">1</span><span class="cl"><span class="kd">type</span> <span class="nx">Profile</span> <span class="kd">struct</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">    <span class="nx">Name</span> <span class="kt">string</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">    <span class="nx">Tags</span> <span class="p">[]</span><span class="kt">string</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>淺層複製：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="ln">1</span><span class="cl"><span class="nx">copyProfile</span> <span class="o">:=</span> <span class="nx">profile</span></span></span></code></pre></div><p><code>copyProfile.Tags</code> 和 <code>profile.Tags</code> 仍然指向同一個底層 array。若要保護邊界，需要複製 slice：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="ln">1</span><span class="cl"><span class="kd">func</span> <span class="nf">CloneProfile</span><span class="p">(</span><span class="nx">p</span> <span class="nx">Profile</span><span class="p">)</span> <span class="nx">Profile</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">    <span class="nx">p</span><span class="p">.</span><span class="nx">Tags</span> <span class="p">=</span> <span class="nb">append</span><span class="p">([]</span><span class="nb">string</span><span class="p">(</span><span class="kc">nil</span><span class="p">),</span> <span class="nx">p</span><span class="p">.</span><span class="nx">Tags</span><span class="o">...</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">    <span class="k">return</span> <span class="nx">p</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>這種 copy 邊界在共享狀態、快取、API response、測試資料中都很重要。</p>
]]></content:encoded></item><item><title>Memcached slab allocator 與記憶體經濟學：明明有記憶體卻在 evict</title><link>https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/memcached/slab-allocator-memory-economics/</link><pubDate>Tue, 16 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/memcached/slab-allocator-memory-economics/</guid><description>&lt;blockquote>
&lt;p>本文是 &lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/memcached/" data-link-title="Memcached" data-link-desc="純記憶體 key-value cache、無持久化">Memcached&lt;/a> overview 的 implementation-layer deep article。選型層（純 KV vs Redis data types、何時選 Memcached）見 overview；本文只處理「決定用 Memcached 後，slab 記憶體怎麼配才不會莫名淘汰」。命令實機驗證於 &lt;code>memcached:1.6&lt;/code>（VERSION 1.6.42）、最後檢查日 2026-06-16；機制以 &lt;a href="https://github.com/memcached/memcached/wiki/UserInternals">Memcached 官方 wiki&lt;/a> 為準。&lt;/p>&lt;/blockquote>
&lt;h2 id="明明有記憶體卻在-evict">明明有記憶體、卻在 evict&lt;/h2>
&lt;p>Memcached 最違反直覺的故障是這樣：監控顯示 &lt;code>evictions&lt;/code> 持續上升、hit rate 在掉，但 &lt;code>stats&lt;/code> 算下來實際用掉的記憶體遠低於 &lt;code>-m&lt;/code> 設的上限——機器明明還有空間，Memcached 卻在淘汰資料。換成 Redis 思維的人會卡住，因為 Redis 是一個共用的記憶體池，不會出現「有空間卻淘汰」。&lt;/p>
&lt;p>這個現象叫 slab calcification，根因在 Memcached 的記憶體模型：它把記憶體預先切成許多固定大小的格子（slab class），每個 class 各自管自己那塊，跟 Redis 共用一個記憶體池的模型相反。記憶體一旦分配給某個 class，預設不會還回去給別的 class 用。如果你的 value 大小分布隨時間改變（早期都是小 value、後來都是大 value），早期被小 value 佔走的記憶體還鎖在小 class 裡，大 value 的 class 沒有足夠空間、開始淘汰——即使整體還有大量「屬於別人」的空閒記憶體。&lt;/p>
&lt;p>理解 Memcached 就是理解這套 slab 經濟學。它用「放棄記憶體的靈活性」換到了「永不碎片化、O(1) 分配、可預測的多執行緒擴展」。這個取捨在純 cache 場景非常划算，但它的失敗模式跟 Redis 完全不同，要用 slab 的語言來判讀。&lt;/p>
&lt;h2 id="核心概念slab-allocator-的會計模型">核心概念：slab allocator 的會計模型&lt;/h2>
&lt;p>Memcached 啟動時不會把 &lt;code>-m&lt;/code> 指定的記憶體一次配掉，而是按需求以 &lt;strong>page&lt;/strong>（預設 1MB）為單位分配給 &lt;strong>slab class&lt;/strong>，每個 class 存放某個大小區間的 item。&lt;/p>
&lt;p>&lt;strong>slab class 與 chunk size&lt;/strong>。每個 slab class 對應一個固定的 chunk size，item 被放進「裝得下它的最小 class」。class 的 chunk size 按 &lt;code>growth_factor&lt;/code> 等比成長——實機看預設值：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="nb">printf&lt;/span> &lt;span class="s1">&amp;#39;stats settings\r\nquit\r\n&amp;#39;&lt;/span> &lt;span class="p">|&lt;/span> nc localhost &lt;span class="m">11211&lt;/span> &lt;span class="p">|&lt;/span> grep growth_factor
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="c1"># STAT growth_factor 1.25&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="nb">printf&lt;/span> &lt;span class="s1">&amp;#39;set k1 0 0 5\r\nhello\r\nstats slabs\r\nquit\r\n&amp;#39;&lt;/span> &lt;span class="p">|&lt;/span> nc localhost &lt;span class="m">11211&lt;/span> &lt;span class="p">|&lt;/span> grep -E &lt;span class="s2">&amp;#34;chunk_size|active_slabs&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="c1"># STAT 1:chunk_size 96 ← 最小的 slab class、chunk 96 bytes&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">&lt;span class="c1"># STAT active_slabs 1&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>growth_factor 1.25&lt;/code> 表示每個 class 的 chunk size 是前一個的 1.25 倍：class 1 是 96 bytes、class 2 約 120、class 3 約 152……一路到 item 大小上限。一個 100 bytes 的 value 放不進 96 bytes 的 class 1，被放進 120 bytes 的 class 2——浪費 20 bytes。這個「向上取整到 chunk size」的浪費是 slab 模型的固有成本。&lt;/p>
&lt;p>&lt;strong>page 分配是單向的&lt;/strong>。當某個 class 需要空間，Memcached 給它一個 1MB 的 page，切成該 class 的 chunk。這個 page 預設永久屬於這個 class——這就是 calcification 的來源。&lt;code>-o slab_automove&lt;/code> 與手動 &lt;code>slabs reassign&lt;/code> 可以把 page 在 class 間搬移，但預設行為偏保守。&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是 <a href="/blog/backend/02-cache-redis/vendors/memcached/" data-link-title="Memcached" data-link-desc="純記憶體 key-value cache、無持久化">Memcached</a> overview 的 implementation-layer deep article。選型層（純 KV vs Redis data types、何時選 Memcached）見 overview；本文只處理「決定用 Memcached 後，slab 記憶體怎麼配才不會莫名淘汰」。命令實機驗證於 <code>memcached:1.6</code>（VERSION 1.6.42）、最後檢查日 2026-06-16；機制以 <a href="https://github.com/memcached/memcached/wiki/UserInternals">Memcached 官方 wiki</a> 為準。</p></blockquote>
<h2 id="明明有記憶體卻在-evict">明明有記憶體、卻在 evict</h2>
<p>Memcached 最違反直覺的故障是這樣：監控顯示 <code>evictions</code> 持續上升、hit rate 在掉，但 <code>stats</code> 算下來實際用掉的記憶體遠低於 <code>-m</code> 設的上限——機器明明還有空間，Memcached 卻在淘汰資料。換成 Redis 思維的人會卡住，因為 Redis 是一個共用的記憶體池，不會出現「有空間卻淘汰」。</p>
<p>這個現象叫 slab calcification，根因在 Memcached 的記憶體模型：它把記憶體預先切成許多固定大小的格子（slab class），每個 class 各自管自己那塊，跟 Redis 共用一個記憶體池的模型相反。記憶體一旦分配給某個 class，預設不會還回去給別的 class 用。如果你的 value 大小分布隨時間改變（早期都是小 value、後來都是大 value），早期被小 value 佔走的記憶體還鎖在小 class 裡，大 value 的 class 沒有足夠空間、開始淘汰——即使整體還有大量「屬於別人」的空閒記憶體。</p>
<p>理解 Memcached 就是理解這套 slab 經濟學。它用「放棄記憶體的靈活性」換到了「永不碎片化、O(1) 分配、可預測的多執行緒擴展」。這個取捨在純 cache 場景非常划算，但它的失敗模式跟 Redis 完全不同，要用 slab 的語言來判讀。</p>
<h2 id="核心概念slab-allocator-的會計模型">核心概念：slab allocator 的會計模型</h2>
<p>Memcached 啟動時不會把 <code>-m</code> 指定的記憶體一次配掉，而是按需求以 <strong>page</strong>（預設 1MB）為單位分配給 <strong>slab class</strong>，每個 class 存放某個大小區間的 item。</p>
<p><strong>slab class 與 chunk size</strong>。每個 slab class 對應一個固定的 chunk size，item 被放進「裝得下它的最小 class」。class 的 chunk size 按 <code>growth_factor</code> 等比成長——實機看預設值：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="nb">printf</span> <span class="s1">&#39;stats settings\r\nquit\r\n&#39;</span> <span class="p">|</span> nc localhost <span class="m">11211</span> <span class="p">|</span> grep growth_factor
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"># STAT growth_factor 1.25</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="nb">printf</span> <span class="s1">&#39;set k1 0 0 5\r\nhello\r\nstats slabs\r\nquit\r\n&#39;</span> <span class="p">|</span> nc localhost <span class="m">11211</span> <span class="p">|</span> grep -E <span class="s2">&#34;chunk_size|active_slabs&#34;</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1"># STAT 1:chunk_size 96      ← 最小的 slab class、chunk 96 bytes</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="c1"># STAT active_slabs 1</span></span></span></code></pre></div><p><code>growth_factor 1.25</code> 表示每個 class 的 chunk size 是前一個的 1.25 倍：class 1 是 96 bytes、class 2 約 120、class 3 約 152……一路到 item 大小上限。一個 100 bytes 的 value 放不進 96 bytes 的 class 1，被放進 120 bytes 的 class 2——浪費 20 bytes。這個「向上取整到 chunk size」的浪費是 slab 模型的固有成本。</p>
<p><strong>page 分配是單向的</strong>。當某個 class 需要空間，Memcached 給它一個 1MB 的 page，切成該 class 的 chunk。這個 page 預設永久屬於這個 class——這就是 calcification 的來源。<code>-o slab_automove</code> 與手動 <code>slabs reassign</code> 可以把 page 在 class 間搬移，但預設行為偏保守。</p>
<p><strong>LRU 是 per-slab-class 的</strong>。淘汰不是全域的，是每個 slab class 維護自己的 LRU。所以「class 2 滿了開始淘汰、但 class 5 有空閒 page」是正常現象——淘汰看的是該 class 自己的空間，不是全域記憶體。</p>
<p>這三點合起來解釋了開頭的悖論：evict 發生在某個 class 內，跟全域剩餘記憶體無關。</p>
<h2 id="配置slab-與多執行緒的設定路徑">配置：slab 與多執行緒的設定路徑</h2>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># 啟動參數（Memcached 的調校多在啟動參數、不像 Redis 有大量 runtime CONFIG SET）</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">docker run -d --name memcached -p 11211:11211 memcached:1.6 <span class="se">\
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="se"></span>  memcached <span class="se">\
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="se"></span>    -m <span class="m">1024</span> <span class="se">\ </span>         <span class="c1"># 記憶體上限 1024 MB</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">    -t <span class="m">4</span> <span class="se">\ </span>            <span class="c1"># worker thread 數（多執行緒、對齊 CPU 核數）</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">    -f 1.25 <span class="se">\ </span>         <span class="c1"># slab growth factor（預設 1.25、調小→class 更密集→浪費更少但 class 更多）</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl">    -I 2m <span class="se">\ </span>           <span class="c1"># 單一 item 大小上限（預設 1MB、超過要調大或拆 value）</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl">    -o <span class="nv">slab_automove</span><span class="o">=</span><span class="m">1</span> <span class="c1"># 自動把空閒 page 從一個 class 搬到吃緊的 class（緩解 calcification）</span></span></span></code></pre></div><p>調校判讀：</p>
<ul>
<li><code>-m</code> 是給 item 資料的上限，Memcached 自身的 hash table、連線 buffer 等 overhead 在 <code>-m</code> 之外，機器要留 headroom</li>
<li><code>-t</code> 對齊 CPU 核數——Memcached 從早期就是 multi-threaded，這是它跟早期單執行緒 Redis 的核心差異</li>
<li><code>-f</code> 調小（例如 1.08）讓 slab class 更密集、向上取整浪費更少，代價是 class 數變多、管理開銷略增</li>
<li><code>-I</code> 是單 item 上限，超過會 store 失敗（見故障演練 Case 3）</li>
<li><code>slab_automove=1</code> 是緩解 calcification 的關鍵，預設視版本而定，明確開啟較穩</li>
</ul>
<h2 id="production-故障演練">Production 故障演練</h2>
<h3 id="case-1slab-calcificationvalue-大小漂移造成假性記憶體不足">Case 1：slab calcification——value 大小漂移造成假性記憶體不足</h3>
<p><strong>徵兆</strong>：<code>evictions</code> 上升、hit rate 下降，但 <code>stats</code> 顯示 <code>bytes</code> 遠低於 <code>limit_maxbytes</code>。<code>stats slabs</code> 看到某個 class 的 page 用滿在淘汰，另一個 class 有大量空閒 chunk。</p>
<p><strong>根因</strong>：value 大小分布隨時間漂移。早期 value 小、記憶體被分配給小 slab class；後來 value 變大、需要大 class，但 page 已被小 class 鎖住不還，大 class 空間不足開始淘汰。整體記憶體沒滿，但「對的 class」沒空間。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>開 <code>-o slab_automove=1</code>，讓 Memcached 自動把空閒 page 從冷 class 搬到吃緊的 class</li>
<li>手動觸發搬移：<code>slabs reassign &lt;src_class&gt; &lt;dst_class&gt;</code>（緊急救火用）</li>
<li>監控 <code>stats slabs</code> 各 class 的 <code>used_chunks</code> vs <code>total_chunks</code> 與 <code>stats items</code> 的 per-class evicted，找出失衡的 class</li>
<li>從源頭穩定 value 大小分布——序列化格式統一、避免同類資料時大時小</li>
</ol>
<h3 id="case-2chunk-向上取整浪費大量記憶體">Case 2：chunk 向上取整浪費大量記憶體</h3>
<p><strong>徵兆</strong>：存的 value 總大小算起來只有 600MB，但 Memcached 報用掉接近 1GB，記憶體效率異常低。</p>
<p><strong>根因</strong>：value 大小剛好落在 slab class chunk size 的「上緣之外」，被向上取整到下一個更大的 class，每個 item 浪費接近一個 growth step 的空間。例如大量 130 bytes 的 value 被放進 152 bytes 的 class，每個浪費 22 bytes，量大就顯著。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><code>-f</code> 調小（1.25 → 1.08）讓 class 粒度更細，向上取整的浪費變小</li>
<li><code>stats slabs</code> 看主要 class 的 <code>chunk_size</code> 跟你的 value 實際大小差多少，量化浪費</li>
<li>value 設計上靠近 chunk 邊界（例如壓縮或裁剪 metadata 讓 value 剛好塞進較小的 class）</li>
<li>浪費是 slab 模型的固有成本，純 KV 的 trade-off——換到的是永不碎片化與 O(1) 分配</li>
</ol>
<h3 id="case-3value-超過-item-大小上限store-直接失敗">Case 3：value 超過 item 大小上限、store 直接失敗</h3>
<p><strong>徵兆</strong>：某些大 value 的寫入回 <code>SERVER_ERROR object too large for cache</code>，application 端 cache 寫入靜默失敗、之後一直 miss。</p>
<p><strong>根因</strong>：單一 item 超過 <code>-I</code> 設的上限（預設 1MB）。Memcached 設計上不適合存大 object，預設 1MB 是刻意的純 cache 邊界。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>確認 value 大小分布，大 value 是否真該進 Memcached（純 KV cache 不適合大 blob）</li>
<li>必要時調大 <code>-I</code>（例如 <code>-I 2m</code>），但這會改變 slab class 結構、增加大 chunk 的記憶體佔用</li>
<li>大 object 考慮壓縮、或拆成多個小 key、或改放適合的儲存（物件儲存 / <a href="/blog/backend/02-cache-redis/vendors/redis/" data-link-title="Redis" data-link-desc="OSS in-memory data structure store、cache 主流">Redis</a> 的 hash）</li>
<li>application 端要處理 store 失敗，不要假設 set 一定成功——失敗就走 origin</li>
</ol>
<h3 id="case-4thread-數設太高lock-contention-反而拖慢">Case 4：thread 數設太高、lock contention 反而拖慢</h3>
<p><strong>徵兆</strong>：把 <code>-t</code> 從 4 調到 32 想榨多核效能，throughput 沒升反降，CPU 在 system time 飆高。</p>
<p><strong>根因</strong>：Memcached 的多執行緒有 per-item lock（hash bucket lock），thread 數遠超核數時，執行緒互搶 lock 與 CPU、context switch 開銷超過平行收益。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><code>-t</code> 對齊實體核數，不要超配（多數場景 4-8 已足夠，極高核機器再往上調並壓測）</li>
<li>用實際 workload 壓測對比不同 <code>-t</code> 的 throughput，找拐點</li>
<li>hot key 集中時 lock contention 更明顯（同 bucket），這是資料分布問題不是 thread 數問題</li>
<li>跨機器水平擴展（client-side consistent hashing）比單機堆 thread 更能解規模，見本文整合段</li>
</ol>
<h3 id="case-5連線數打到上限新連線被拒">Case 5：連線數打到上限、新連線被拒</h3>
<p><strong>徵兆</strong>：高並發下新連線報錯或 hang，<code>stats</code> 的 <code>curr_connections</code> 接近 <code>max_connections</code>，<code>listen_disabled_num</code> 在增加。</p>
<p><strong>根因</strong>：每個 client 連線佔一個 connection slot，Memcached 預設 <code>-c 1024</code>。大量 client（尤其沒用連線池、每請求建連）會打滿 connection 上限。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>client 端用連線池重用連線，不要每請求建連</li>
<li>調高 <code>-c</code>（例如 <code>-c 4096</code>），但連線本身有記憶體 overhead（在 <code>-m</code> 之外），要算進機器容量</li>
<li>監控 <code>curr_connections</code> 與 <code>listen_disabled_num</code>，後者非零代表曾達上限拒絕連線</li>
<li>連線數爆炸常是 client fan-out 問題，跨多 Memcached node 分散（consistent hashing）能攤平單 node 連線壓力</li>
</ol>
<h2 id="capacity--cost-邊界">Capacity / cost 邊界</h2>
<p>Memcached 的容量判讀，核心在 slab 效率與多執行緒擴展：</p>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>健康區間</th>
          <th>警戒與動作</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>evictions</code> 速率</td>
          <td>接近 0（working set 放得下）</td>
          <td>持續高但記憶體沒滿 → calcification、開 slab_automove</td>
      </tr>
      <tr>
          <td>各 class <code>used / total chunks</code></td>
          <td>各 class 均衡</td>
          <td>單 class 滿、其他空 → calcification</td>
      </tr>
      <tr>
          <td>chunk 向上取整浪費</td>
          <td>小（value 貼近 chunk size）</td>
          <td>大 → 調小 <code>-f</code> 或調整 value 大小</td>
      </tr>
      <tr>
          <td><code>curr_connections / -c</code></td>
          <td>&lt; 80%</td>
          <td>接近上限 → 用連線池或調高 <code>-c</code></td>
      </tr>
      <tr>
          <td>多執行緒 CPU</td>
          <td>核數內、system time 低</td>
          <td>system time 高 → <code>-t</code> 超配、lock contention</td>
      </tr>
  </tbody>
</table>
<p>撞牆後的路由判斷：</p>
<ul>
<li><strong>需要 data types / 持久化 / distributed lock</strong>：Memcached 是純 KV、刻意不做這些。需要這些走 <a href="/blog/backend/02-cache-redis/vendors/redis/" data-link-title="Redis" data-link-desc="OSS in-memory data structure store、cache 主流">Redis / Valkey</a>，這是 capability 差異不是調校能補。</li>
<li><strong>單機容量 / throughput 不夠</strong>：Memcached 沒有 server-side cluster，靠 client-side consistent hashing（ketama）水平擴展到多 node，見整合。</li>
<li><strong>想要 Memcached 的多執行緒 + Redis 的 data types</strong>：<a href="/blog/backend/02-cache-redis/vendors/dragonflydb/" data-link-title="DragonflyDB" data-link-desc="高效能 Redis / Memcached 相容替代、多核架構">DragonflyDB</a> 兼具多核與 Redis 相容，是兩者的中間點。</li>
</ul>
<h2 id="整合--下一步">整合 / 下一步</h2>
<p>Memcached 的單機很簡單，它的工程深度在「如何把多個 Memcached node 組成一個 cache 層」——而這發生在 client 端與代理層，不在 server：</p>
<ul>
<li><strong>client-side consistent hashing（ketama）</strong>：Memcached server 之間互不知道彼此，sharding 由 client library 用 consistent hashing 決定 key 去哪個 node，加減 node 時最小化 key 重新分布。這是 Memcached 水平擴展的基礎。</li>
<li><strong>跟 <a href="/blog/backend/02-cache-redis/cases/meta-mcrouter-global-cache-routing/" data-link-title="2.C2 Meta：mcrouter 與跨區快取路由" data-link-desc="快取從單點最佳化演進到分散式路由層的案例。">Meta mcrouter</a></strong>：Meta 的 mcrouter 是 Memcached 專屬的 protocol-aware routing proxy，把跨叢集 / 跨區的流量收斂、失效隔離、pool 管理從 client 端移到代理層——這是 Memcached 大規模治理的標準答案。</li>
<li><strong>跟 <a href="/blog/backend/02-cache-redis/cases/netflix-evcache-global-cache-layer/" data-link-title="2.C6 Netflix：EVCache 全域快取層" data-link-desc="快取從本地層演進為跨區分散式能力的案例。">Netflix EVCache</a></strong>：EVCache 基於 Memcached，Netflix 在上面加跨 AZ replication 與 client-side smart routing，補足 Memcached 沒有的跨區 HA。</li>
<li><strong>跟 <a href="/blog/backend/02-cache-redis/cases/meta-tao-social-graph-cache-evolution/" data-link-title="2.C8 Meta：TAO 社交圖快取演進" data-link-desc="社交圖查詢在規模化下如何把快取做成資料層能力。">Meta TAO</a></strong>：TAO 底層用 Memcached 作為 social graph 的 cache 層，上層加一致性與關聯查詢——展示了純 KV 之上如何疊加語意。</li>
<li><strong>跟 <a href="/blog/backend/02-cache-redis/cases/meta-cachelib-kangaroo-tiered-cache/" data-link-title="2.C4 Meta：CacheLib / Kangaroo 分層快取" data-link-desc="快取從 DRAM-only 轉向分層快取架構的實務案例。">Meta CacheLib + Kangaroo</a></strong>：當 DRAM 的記憶體經濟撞到極限，Meta 用 CacheLib 把 cache 分層到 flash——這是 slab 記憶體經濟學的下一個邊界。</li>
</ul>
<h2 id="相關連結">相關連結</h2>
<ul>
<li>上游 vendor 頁：<a href="/blog/backend/02-cache-redis/vendors/memcached/" data-link-title="Memcached" data-link-desc="純記憶體 key-value cache、無持久化">Memcached</a></li>
<li>對照 vendor：<a href="/blog/backend/02-cache-redis/vendors/redis/memory-eviction-tuning/" data-link-title="Redis 記憶體與淘汰調校：maxmemory-policy、LFU 與碎片化的實戰判讀" data-link-desc="Redis 的記憶體是一條會在半夜爆掉的曲線：maxmemory 設多少、policy 選 LRU 還 LFU、碎片化什麼時候開始吃掉 30% RAM、OOM 時 noeviction 怎麼讓寫入全部失敗。本文展開 Redis 記憶體會計模型、eviction policy 的選型判讀、5 個把記憶體配置寫成 production 事故的踩坑，以及單機記憶體撞牆後該往 cluster 還是 DragonflyDB 走的邊界">Redis 記憶體與淘汰調校</a>（jemalloc 池 vs slab class 的差異）、<a href="/blog/backend/02-cache-redis/vendors/dragonflydb/" data-link-title="DragonflyDB" data-link-desc="高效能 Redis / Memcached 相容替代、多核架構">DragonflyDB</a></li>
<li>相關 migration：<a href="/blog/backend/02-cache-redis/vendors/redis/migrate-to-memcached/" data-link-title="Redis → Memcached：Memcached 不是 simpler Redis、是 cache paradigm" data-link-desc="Redis → Memcached 是 Type E paradigm reduction migration — 從 multi-paradigm（KV &#43; 資料結構 &#43; pub/sub &#43; Lua &#43; streams）退到 pure cache；不是「remove Redis features」、是「重新分配 Redis-specific feature 到對應 specialized 服務」；5 個 production 踩雷 &#43; paradigm reduction 路線">Redis → Memcached</a></li>
<li>Methodology：<a href="/blog/posts/vendor-%E6%B7%B1%E5%BA%A6%E6%8A%80%E8%A1%93%E6%96%87%E7%AB%A0%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84%E5%90%8C-vendor-%E7%B3%BB%E5%88%97%E7%9A%84%E9%96%8B%E5%A0%B4%E8%BC%AA%E6%9B%BF%E9%A9%97%E8%AD%89/" data-link-title="Vendor 深度技術文章方法論的演化紀錄：同 vendor 系列的開場輪替驗證" data-link-desc="vendor overview 飽和後要寫單一功能深度文章、需要選題與結構依據時回來。這套方法論的驗證來源與 cadence variant 在高風險場景（同 vendor sub-tool 系列）的實證。">Vendor 深度技術文章寫作方法論</a></li>
</ul>
]]></content:encoded></item><item><title>Redis 記憶體與淘汰調校：maxmemory-policy、LFU 與碎片化的實戰判讀</title><link>https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/redis/memory-eviction-tuning/</link><pubDate>Tue, 16 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/redis/memory-eviction-tuning/</guid><description>&lt;blockquote>
&lt;p>本文是 &lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/redis/" data-link-title="Redis" data-link-desc="OSS in-memory data structure store、cache 主流">Redis&lt;/a> overview 的 implementation-layer deep article。選型層（Redis vs Valkey vs Memcached）見 overview；本文只處理「已經選了 Redis、記憶體怎麼配才不會在尖峰爆掉」。配置以 &lt;a href="https://redis.io/docs/latest/operate/oss_and_stack/management/optimization/memory-optimization/">Redis 官方 memory optimization 文件&lt;/a> 為準、最後檢查日 2026-06-16。&lt;/p>&lt;/blockquote>
&lt;h2 id="你的-redis-會在凌晨三點-oom">你的 Redis 會在凌晨三點 OOM&lt;/h2>
&lt;p>Redis 的記憶體問題很少在有人盯著儀表板時發生。它發生在流量爬升、某個 key 集合悄悄長大、AOF rewrite 剛好撞上 RDB save 的那個瞬間——通常是凌晨三點，沒人盯著。徵兆是 application 端突然一片 &lt;code>OOM command not allowed when used memory &amp;gt; 'maxmemory'&lt;/code>，所有寫入失敗，但讀取還活著，於是監控的「Redis 還在回應」綠燈騙過了 on-call。&lt;/p>
&lt;p>這類事故的根因幾乎都不是「Redis 不夠快」，而是三個記憶體旋鈕在設計時被當成預設值放著沒動：&lt;code>maxmemory&lt;/code> 設多少、&lt;code>maxmemory-policy&lt;/code> 選哪個、以及沒人注意到的記憶體碎片化。這三個旋鈕決定了 Redis 在記憶體壓力下是「優雅地淘汰冷資料繼續服務」還是「拒絕所有寫入直到有人重啟」。本文處理這三者的會計模型、選型判讀，以及它們怎麼被寫成事故。&lt;/p>
&lt;p>對延遲就是業務 KPI 的服務，這個旋鈕的代價更直接。&lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/tinder-elasticache-valkey-matching/" data-link-title="9.C6 Tinder：ElastiCache for Valkey 撐 4700 萬月活的配對引擎" data-link-desc="Tinder 用 Amazon ElastiCache for Valkey 提供配對引擎所需的次毫秒延遲快取層">Tinder 的配對引擎&lt;/a>每次滑動要查多個快取（profile、距離、偏好、推薦池），4700 萬月活下 cache 不是 DB 的補救、是主要服務面，cache miss 是邊緣案例。eviction policy 選錯，淘汰掉的若是熱資料，下一次滑動就打回 origin，sub-millisecond 的延遲預算瞬間破表。&lt;/p>
&lt;h2 id="核心概念redis-記憶體的會計模型">核心概念：Redis 記憶體的會計模型&lt;/h2>
&lt;p>要調校記憶體，先要分清楚 &lt;code>used_memory&lt;/code> 這個數字到底由什麼組成。&lt;code>INFO memory&lt;/code> 回報的是幾層疊加的記憶體會計，每一層去處不同：&lt;/p>
&lt;p>&lt;strong>&lt;code>used_memory&lt;/code>&lt;/strong> 是 Redis allocator（預設 jemalloc）配給資料、結構與 buffer 的總量。&lt;strong>&lt;code>used_memory_rss&lt;/code>&lt;/strong> 是作業系統視角看到的 Redis 進程實體記憶體（resident set size），它通常大於 &lt;code>used_memory&lt;/code>——兩者的比值就是 &lt;code>mem_fragmentation_ratio&lt;/code>。&lt;strong>&lt;code>used_memory_dataset&lt;/code>&lt;/strong> 才是純資料的部分，扣掉了 Redis 自身的 overhead。&lt;/p>
&lt;p>理解三個跟 OOM 直接相關的記憶體去處：&lt;/p>
&lt;p>&lt;strong>資料本身的編碼會放大或縮小記憶體&lt;/strong>。一個小 hash（field 數少於 &lt;code>hash-max-listpack-entries&lt;/code>、value 短於 &lt;code>hash-max-listpack-value&lt;/code>）用 listpack 緊湊編碼，記憶體可能只有大 hash 用 hashtable 編碼的幾分之一。同樣的邏輯套用在 list、set、sorted set。一個欄位設計的小決定（把 user object 拆成 200 個獨立 key 還是壓成一個 hash）會讓記憶體差好幾倍。&lt;/p>
&lt;p>&lt;strong>client output buffer 不計入 dataset 但會吃光記憶體&lt;/strong>。慢速 consumer、&lt;code>MONITOR&lt;/code>、大量 pub/sub 訂閱者都會讓 Redis 在 server 端堆積 reply buffer。&lt;code>client-output-buffer-limit&lt;/code> 沒設好，一個讀很慢的 replica 或一個掛著的 &lt;code>MONITOR&lt;/code> 連線就能把記憶體推到 maxmemory。&lt;/p>
&lt;p>&lt;strong>fork 期間記憶體會短暫翻倍&lt;/strong>。RDB save 與 AOF rewrite 都靠 &lt;code>fork()&lt;/code> + copy-on-write，父進程在 fork 後若持續寫入，被改動的 page 會被複製，最壞情況記憶體接近翻倍。這是 maxmemory 必須留 headroom 的核心原因，細節見 &lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/redis/persistence-fork-latency/" data-link-title="Redis 持久化與 fork latency：AOF、RDB 與那一次卡住整個 cluster 的 fork" data-link-desc="Redis 的 RDB save 與 AOF rewrite 都靠一次 fork()，而 fork 在大記憶體實例上會凍結主執行緒數百毫秒、複製分頁讓記憶體逼近翻倍。本文展開 AOF / RDB 的機制與 fsync 取捨、copy-on-write 的記憶體放大、5 個把持久化寫成延遲尖峰與資料遺失的 production 踩坑，以及 cache 場景到底要不要持久化的邊界">persistence 與 fork latency deep article&lt;/a>。&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是 <a href="/blog/backend/02-cache-redis/vendors/redis/" data-link-title="Redis" data-link-desc="OSS in-memory data structure store、cache 主流">Redis</a> overview 的 implementation-layer deep article。選型層（Redis vs Valkey vs Memcached）見 overview；本文只處理「已經選了 Redis、記憶體怎麼配才不會在尖峰爆掉」。配置以 <a href="https://redis.io/docs/latest/operate/oss_and_stack/management/optimization/memory-optimization/">Redis 官方 memory optimization 文件</a> 為準、最後檢查日 2026-06-16。</p></blockquote>
<h2 id="你的-redis-會在凌晨三點-oom">你的 Redis 會在凌晨三點 OOM</h2>
<p>Redis 的記憶體問題很少在有人盯著儀表板時發生。它發生在流量爬升、某個 key 集合悄悄長大、AOF rewrite 剛好撞上 RDB save 的那個瞬間——通常是凌晨三點，沒人盯著。徵兆是 application 端突然一片 <code>OOM command not allowed when used memory &gt; 'maxmemory'</code>，所有寫入失敗，但讀取還活著，於是監控的「Redis 還在回應」綠燈騙過了 on-call。</p>
<p>這類事故的根因幾乎都不是「Redis 不夠快」，而是三個記憶體旋鈕在設計時被當成預設值放著沒動：<code>maxmemory</code> 設多少、<code>maxmemory-policy</code> 選哪個、以及沒人注意到的記憶體碎片化。這三個旋鈕決定了 Redis 在記憶體壓力下是「優雅地淘汰冷資料繼續服務」還是「拒絕所有寫入直到有人重啟」。本文處理這三者的會計模型、選型判讀，以及它們怎麼被寫成事故。</p>
<p>對延遲就是業務 KPI 的服務，這個旋鈕的代價更直接。<a href="/blog/backend/09-performance-capacity/cases/tinder-elasticache-valkey-matching/" data-link-title="9.C6 Tinder：ElastiCache for Valkey 撐 4700 萬月活的配對引擎" data-link-desc="Tinder 用 Amazon ElastiCache for Valkey 提供配對引擎所需的次毫秒延遲快取層">Tinder 的配對引擎</a>每次滑動要查多個快取（profile、距離、偏好、推薦池），4700 萬月活下 cache 不是 DB 的補救、是主要服務面，cache miss 是邊緣案例。eviction policy 選錯，淘汰掉的若是熱資料，下一次滑動就打回 origin，sub-millisecond 的延遲預算瞬間破表。</p>
<h2 id="核心概念redis-記憶體的會計模型">核心概念：Redis 記憶體的會計模型</h2>
<p>要調校記憶體，先要分清楚 <code>used_memory</code> 這個數字到底由什麼組成。<code>INFO memory</code> 回報的是幾層疊加的記憶體會計，每一層去處不同：</p>
<p><strong><code>used_memory</code></strong> 是 Redis allocator（預設 jemalloc）配給資料、結構與 buffer 的總量。<strong><code>used_memory_rss</code></strong> 是作業系統視角看到的 Redis 進程實體記憶體（resident set size），它通常大於 <code>used_memory</code>——兩者的比值就是 <code>mem_fragmentation_ratio</code>。<strong><code>used_memory_dataset</code></strong> 才是純資料的部分，扣掉了 Redis 自身的 overhead。</p>
<p>理解三個跟 OOM 直接相關的記憶體去處：</p>
<p><strong>資料本身的編碼會放大或縮小記憶體</strong>。一個小 hash（field 數少於 <code>hash-max-listpack-entries</code>、value 短於 <code>hash-max-listpack-value</code>）用 listpack 緊湊編碼，記憶體可能只有大 hash 用 hashtable 編碼的幾分之一。同樣的邏輯套用在 list、set、sorted set。一個欄位設計的小決定（把 user object 拆成 200 個獨立 key 還是壓成一個 hash）會讓記憶體差好幾倍。</p>
<p><strong>client output buffer 不計入 dataset 但會吃光記憶體</strong>。慢速 consumer、<code>MONITOR</code>、大量 pub/sub 訂閱者都會讓 Redis 在 server 端堆積 reply buffer。<code>client-output-buffer-limit</code> 沒設好，一個讀很慢的 replica 或一個掛著的 <code>MONITOR</code> 連線就能把記憶體推到 maxmemory。</p>
<p><strong>fork 期間記憶體會短暫翻倍</strong>。RDB save 與 AOF rewrite 都靠 <code>fork()</code> + copy-on-write，父進程在 fork 後若持續寫入，被改動的 page 會被複製，最壞情況記憶體接近翻倍。這是 maxmemory 必須留 headroom 的核心原因，細節見 <a href="/blog/backend/02-cache-redis/vendors/redis/persistence-fork-latency/" data-link-title="Redis 持久化與 fork latency：AOF、RDB 與那一次卡住整個 cluster 的 fork" data-link-desc="Redis 的 RDB save 與 AOF rewrite 都靠一次 fork()，而 fork 在大記憶體實例上會凍結主執行緒數百毫秒、複製分頁讓記憶體逼近翻倍。本文展開 AOF / RDB 的機制與 fsync 取捨、copy-on-write 的記憶體放大、5 個把持久化寫成延遲尖峰與資料遺失的 production 踩坑，以及 cache 場景到底要不要持久化的邊界">persistence 與 fork latency deep article</a>。</p>
<p><code>maxmemory</code> 框住的是 <code>used_memory</code>，不是 <code>used_memory_rss</code>。所以 maxmemory 設成機器 RAM 的 100% 是錯的——碎片化、fork copy-on-write、client buffer 都在 maxmemory 之外，會把 RSS 推爆系統，觸發 Linux OOM killer 直接砍掉 Redis 進程（比 Redis 自己的 noeviction 更糟，因為是無預警 SIGKILL）。</p>
<h2 id="配置maxmemory-與-policy-的設定路徑">配置：maxmemory 與 policy 的設定路徑</h2>
<p>設定分兩步：先框住記憶體上限，再決定撞到上限時的行為。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1"># 1. 設定記憶體上限（留 headroom 給 fork / fragmentation / client buffer）</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1"># 機器 RAM 8GB → maxmemory 設 ~5-6GB、留 25-35% headroom</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">redis-cli CONFIG SET maxmemory 6gb
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">
</span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="c1"># 2. 設定撞到上限時的淘汰行為</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">redis-cli CONFIG SET maxmemory-policy allkeys-lfu
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="c1"># 3. 永久化到 redis.conf（CONFIG SET 重啟後失效）</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="c1"># redis.conf:</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="c1">#   maxmemory 6gb</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="c1">#   maxmemory-policy allkeys-lfu</span></span></span></code></pre></div><p>八個 <code>maxmemory-policy</code> 選項分三類，選型靠「資料是不是全部都能淘汰」與「淘汰要靠存取頻率還是 TTL」兩個問題：</p>
<table>
  <thead>
      <tr>
          <th>policy</th>
          <th>淘汰範圍</th>
          <th>淘汰依據</th>
          <th>適用場景</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>noeviction</code></td>
          <td>不淘汰</td>
          <td>寫入直接報錯</td>
          <td>資料是 source-of-truth、不能丟（少見）</td>
      </tr>
      <tr>
          <td><code>allkeys-lru</code></td>
          <td>所有 key</td>
          <td>最近最少使用</td>
          <td>純 cache、無法預判哪些該留</td>
      </tr>
      <tr>
          <td><code>allkeys-lfu</code></td>
          <td>所有 key</td>
          <td>最少使用頻率</td>
          <td>純 cache、有明顯熱資料（多數 cache 場景）</td>
      </tr>
      <tr>
          <td><code>allkeys-random</code></td>
          <td>所有 key</td>
          <td>隨機</td>
          <td>key 存取均勻、省 LRU/LFU 計算</td>
      </tr>
      <tr>
          <td><code>volatile-lru</code></td>
          <td>有 TTL 的 key</td>
          <td>最近最少使用</td>
          <td>cache 與持久資料混存、只淘汰可過期的</td>
      </tr>
      <tr>
          <td><code>volatile-lfu</code></td>
          <td>有 TTL 的 key</td>
          <td>最少使用頻率</td>
          <td>同上、有熱資料</td>
      </tr>
      <tr>
          <td><code>volatile-random</code></td>
          <td>有 TTL 的 key</td>
          <td>隨機</td>
          <td>同上、省計算</td>
      </tr>
      <tr>
          <td><code>volatile-ttl</code></td>
          <td>有 TTL 的 key</td>
          <td>最接近過期的先淘汰</td>
          <td>想讓近期過期的提早讓位</td>
      </tr>
  </tbody>
</table>
<h3 id="lru-跟-lfu-的真實差異">LRU 跟 LFU 的真實差異</h3>
<p><code>allkeys-lru</code> 跟 <code>allkeys-lfu</code> 看起來像同一件事的兩種寫法，但選錯會在特定 workload 下讓 hit rate 掉一截。LRU 看「最後一次被存取是多久以前」，LFU 看「被存取的頻率」。差別在一次性掃描（scan pollution）：某個批次任務一次讀過大量冷 key，LRU 會把這些剛被碰過的冷 key 排到淘汰隊伍最後面，反而把真正的熱 key 擠出去。LFU 因為看頻率，一次性的存取不會讓冷 key 假裝成熱 key。</p>
<p>Redis 4.0 後的 LFU 用的是 probabilistic counter（Morris counter）加 decay，不是精確計數，靠兩個參數調：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># lfu-log-factor：counter 增長的對數速度、越大越能區分高頻 key</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">redis-cli CONFIG SET lfu-log-factor <span class="m">10</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1"># lfu-decay-time：counter 衰減的分鐘數、越小越快遺忘舊熱度</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">redis-cli CONFIG SET lfu-decay-time <span class="m">1</span></span></span></code></pre></div><p>對 <a href="/blog/backend/09-performance-capacity/cases/tinder-elasticache-valkey-matching/" data-link-title="9.C6 Tinder：ElastiCache for Valkey 撐 4700 萬月活的配對引擎" data-link-desc="Tinder 用 Amazon ElastiCache for Valkey 提供配對引擎所需的次毫秒延遲快取層">Tinder 這類有明顯熱資料</a>（熱門 profile、熱區域推薦池）的服務，<code>allkeys-lfu</code> 比 <code>allkeys-lru</code> 更能保護熱 key 不被批次掃描或冷流量擠出。</p>
<h3 id="approximate-eviction-的取樣">approximate eviction 的取樣</h3>
<p>Redis 的 LRU/LFU 都是近似演算法，不掃全 keyspace，而是每次取樣 <code>maxmemory-samples</code> 個 key（預設 5）挑最該淘汰的。樣本數越大越接近精確 LRU/LFU，但越吃 CPU。記憶體壓力大、淘汰頻繁時，預設 5 已夠；要更精準可調到 10，代價是淘汰路徑的 CPU 上升。</p>
<h2 id="production-故障演練">Production 故障演練</h2>
<h3 id="case-1noeviction-讓寫入全滅讀取假裝健康">Case 1：noeviction 讓寫入全滅、讀取假裝健康</h3>
<p><strong>徵兆</strong>：application 寫入路徑大量 <code>OOM command not allowed when used memory &gt; 'maxmemory'</code>，但 <code>GET</code> 仍正常、health check（通常打 <code>PING</code> 或 <code>GET</code>）綠燈，on-call 收到的是 application 層的 500、不是 Redis 告警。</p>
<p><strong>根因</strong>：<code>maxmemory-policy</code> 預設是 <code>noeviction</code>。當 Redis 把 cache 當 cache 用，但 policy 留在 <code>noeviction</code>，記憶體一滿，所有會增加記憶體的命令（<code>SET</code>、<code>LPUSH</code>、<code>HSET</code>）直接報錯，唯讀命令照常。health check 若只測讀取，完全偵測不到。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>純 cache 場景把 policy 改成 <code>allkeys-lru</code> 或 <code>allkeys-lfu</code>，讓記憶體壓力自動透過淘汰釋放</li>
<li>health check 加一個寫入探針（<code>SET healthcheck:probe &lt;ts&gt; EX 10</code>），讓 OOM 寫入失敗能被偵測</li>
<li>告警掛在 <code>used_memory / maxmemory &gt; 0.85</code>，不要等 OOM 才反應</li>
<li>若資料真的不能淘汰（誤把 Redis 當 source-of-truth），那不該用 cache 配置，見本文 Capacity / cost 邊界段的路由判斷</li>
</ol>
<h3 id="case-2碎片化吃掉-30-記憶體">Case 2：碎片化吃掉 30% 記憶體</h3>
<p><strong>徵兆</strong>：<code>used_memory</code> 顯示 4GB、但 <code>used_memory_rss</code> 是 5.5GB，<code>mem_fragmentation_ratio</code> 是 1.37，機器 RAM 開始吃緊但資料量沒漲。重啟 Redis 後 RSS 掉回 4GB 出頭。</p>
<p><strong>根因</strong>：大量寫入後刪除、或 value 大小頻繁變動（例如 list 一直 push/pop），jemalloc 的記憶體頁出現空洞——配出去的 page 還佔著 RSS，但裡面只有零星資料。<code>mem_fragmentation_ratio</code> 持續 &gt; 1.5 是明確訊號。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>
<p>開 active defrag 讓 Redis 在背景整理（4.0+）：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">redis-cli CONFIG SET activedefrag yes
</span></span><span class="line"><span class="ln">2</span><span class="cl">redis-cli CONFIG SET active-defrag-ignore-bytes 100mb
</span></span><span class="line"><span class="ln">3</span><span class="cl">redis-cli CONFIG SET active-defrag-threshold-lower <span class="m">10</span></span></span></code></pre></div></li>
<li>
<p>fragmentation_ratio &lt; 1.0 是另一種警訊——代表 Redis 在用 swap，比碎片化更危險，要立刻降記憶體壓力</p>
</li>
<li>
<p>結構選擇上避免大幅波動的 collection；穩態大小的資料碎片化天然較低</p>
</li>
<li>
<p>計算 maxmemory headroom 時把 1.2-1.4 的 fragmentation 算進去</p>
</li>
</ol>
<h3 id="case-3一個-monitor-連線把記憶體推爆">Case 3：一個 MONITOR 連線把記憶體推爆</h3>
<p><strong>徵兆</strong>：某次 debug 後記憶體莫名持續上升，<code>used_memory_dataset</code> 沒變但 <code>used_memory</code> 一直漲，<code>CLIENT LIST</code> 看到一個連線的 <code>omem</code>（output buffer memory）有幾百 MB。</p>
<p><strong>根因</strong>：有人開了 <code>MONITOR</code> 去看即時命令流、然後忘了關（或 client crash 但連線沒斷）。<code>MONITOR</code> 把每一條命令都推給該連線，高 QPS 下 server 端 output buffer 爆量堆積，計入 <code>used_memory</code> 但不在 dataset。慢速 replica 或大量 pub/sub 訂閱者也會觸發同類問題。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>
<p>設定 client output buffer 上限，超過就斷線：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># normal client / replica / pubsub 分開設</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">redis-cli CONFIG SET client-output-buffer-limit <span class="s2">&#34;normal 256mb 64mb 60&#34;</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">redis-cli CONFIG SET client-output-buffer-limit <span class="s2">&#34;pubsub 32mb 8mb 60&#34;</span></span></span></code></pre></div></li>
<li>
<p><code>MONITOR</code> 在 production 嚴格禁用或限時，它本身也拖慢整個 server</p>
</li>
<li>
<p>監控加 <code>CLIENT LIST</code> 的 <code>omem</code> 巡檢，找出異常 buffer 的連線</p>
</li>
<li>
<p>replica lag 過大時 output buffer 會堆，對應 <a href="/blog/backend/02-cache-redis/vendors/redis/sentinel-ha-failover/" data-link-title="Redis Sentinel 與 failover 時序：從 master 死掉到 client 重連的每一段" data-link-desc="Redis Sentinel 的 failover 不是一個瞬間動作，是 down 偵測 → quorum 確認 → 選主 → 提升 → 配置廣播 → client 重連的一條時序鏈，每一段都有自己的延遲與失敗模式。本文展開 Sentinel 的判定模型與這條時序、5 個讓 failover 卡住或丟資料的 production 踩坑，以及 Sentinel 撐不住該往 Cluster 或 managed 走的邊界">replication / failover deep article</a></p>
</li>
</ol>
<h3 id="case-4欄位設計讓記憶體多用三倍">Case 4：欄位設計讓記憶體多用三倍</h3>
<p><strong>徵兆</strong>：資料筆數跟預估一致，但 <code>used_memory</code> 是試算的 3 倍。<code>MEMORY USAGE &lt;key&gt;</code> 抽樣發現單筆 object 的記憶體遠超 value 本身的 byte 數。</p>
<p><strong>根因</strong>：把一個有 10 個欄位的 user object 拆成 10 個獨立 string key（<code>user:123:name</code>、<code>user:123:age</code>&hellip;），每個 key 都帶 Redis 的 key overhead（dict entry、expire dict entry、key 字串本身）。10 個 key 的 overhead 是一個 hash 的好幾倍。反過來，超過 <code>hash-max-listpack-entries</code> 的大 hash 從緊湊的 listpack 退化成 hashtable 編碼，也會放大記憶體。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>
<p>同一 entity 的欄位用一個 hash 存，共享 key overhead</p>
</li>
<li>
<p>保持 hash 在 listpack 閾值內以用緊湊編碼：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">redis-cli CONFIG GET hash-max-listpack-entries  <span class="c1"># 預設 128</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">redis-cli CONFIG GET hash-max-listpack-value    <span class="c1"># 預設 64</span></span></span></code></pre></div></li>
<li>
<p>用 <code>MEMORY USAGE &lt;key&gt;</code> 跟 <code>redis-cli --bigkeys</code> 抽樣驗證實際記憶體，不靠試算</p>
</li>
<li>
<p><a href="/blog/backend/02-cache-redis/cases/shopify-cache-serialization-migration/" data-link-title="2.C3 Shopify：快取序列化格式遷移" data-link-desc="快取 payload 從 Marshal 轉 MessagePack 的遷移策略。">Shopify 的 serialization 遷移</a>（Marshal → MessagePack）正是用更省的編碼壓 payload，欄位編碼決策對記憶體與頻寬同時有效</p>
</li>
</ol>
<h3 id="case-5淘汰把熱-key-一起帶走hit-rate-崩">Case 5：淘汰把熱 key 一起帶走、hit rate 崩</h3>
<p><strong>徵兆</strong>：記憶體壓力下開始 eviction（<code>evicted_keys</code> 持續上升），同時 <code>keyspace_hits / (hits + misses)</code> 從 95% 掉到 70%，origin QPS 跟著飆，下游 DB 開始吃緊。</p>
<p><strong>根因</strong>：用了 <code>allkeys-random</code>，或 <code>allkeys-lru</code> 撞上批次掃描污染，淘汰演算法把熱 key 跟冷 key 一視同仁，熱 key 被淘汰後下一個請求 miss、回源、再寫回，形成淘汰與回填的拉鋸，hit rate 持續惡化。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>有明顯熱資料就用 <code>allkeys-lfu</code>，讓頻率高的 key 留下</li>
<li>把 maxmemory-samples 調到 10 提高淘汰精準度</li>
<li>根因常是記憶體真的不夠——<code>evicted_keys</code> 持續高代表 working set 超過 maxmemory，該擴容或分片，不是純調 policy 能解</li>
<li>熱 key 本身過熱（單 key QPS 遠超其他）要走 local cache + Redis 兩層，對應 <a href="/blog/backend/02-cache-redis/high-concurrency-access/" data-link-title="2.1 高併發下的 Redis 讀寫邊界" data-link-desc="說明高併發服務如何共用 Redis client、控制 pipeline 與避免 cache stampede">2.6 high concurrency</a></li>
</ol>
<h2 id="capacity--cost-邊界">Capacity / cost 邊界</h2>
<p>記憶體配置的容量判讀，核心是「working set 對 maxmemory 的比值」與「淘汰是否健康」：</p>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>健康區間</th>
          <th>警戒與動作</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>used_memory / maxmemory</code></td>
          <td>&lt; 80%</td>
          <td>&gt; 85% 告警、&gt; 95% 接近 OOM 或大量淘汰</td>
      </tr>
      <tr>
          <td><code>mem_fragmentation_ratio</code></td>
          <td>1.0 - 1.5</td>
          <td>&gt; 1.5 開 active defrag、&lt; 1.0 在用 swap 要救火</td>
      </tr>
      <tr>
          <td><code>evicted_keys</code> 速率</td>
          <td>接近 0（working set 放得下）</td>
          <td>持續高 → working set 超量、該擴容 / 分片</td>
      </tr>
      <tr>
          <td>hit rate</td>
          <td>&gt; 90%（多數 cache）</td>
          <td>持續下滑 → 淘汰太兇或 TTL 太短</td>
      </tr>
      <tr>
          <td>fork 期間 RSS 峰值</td>
          <td>&lt; 機器 RAM</td>
          <td>接近 RAM → maxmemory headroom 不足、降 maxmemory</td>
      </tr>
  </tbody>
</table>
<p>撞牆後的路由判斷：</p>
<ul>
<li><strong>單機記憶體不夠、working set 持續超量</strong>：垂直擴容（換更大記憶體機型）是第一步，但有單機上限。超過後走 <a href="/blog/backend/02-cache-redis/vendors/redis/cluster-resharding/" data-link-title="Redis Cluster Re-sharding：source = target，但 topology 重劃的 5 段流程" data-link-desc="Redis cluster re-sharding 是 5 type migration 漏類實證 — source / target 同 cluster、無 schema / paradigm 差、但 16384 slot 重分配是核心；本文涵蓋 4 種 re-sharding driver、slot migration 機制、redis-cli --cluster rebalance / reshard 工具、5 個 production 踩雷（cluster busy / replica lag / client cache stale / cross-slot transaction / monitor gap）">Redis Cluster 分片</a>，把 keyspace 切到多 node。</li>
<li><strong>想用 Redis API 但要極致單機記憶體效率</strong>：<a href="/blog/backend/02-cache-redis/vendors/dragonflydb/" data-link-title="DragonflyDB" data-link-desc="高效能 Redis / Memcached 相容替代、多核架構">DragonflyDB</a> 的 dashtable 在同 dataset 下通常比 Redis 省 20-40% 記憶體（依資料形狀、以官方 benchmark 為準），且單機多核能撐到 Redis 要靠 cluster 才能達到的規模——若 cluster re-sharding 頻繁觸發，評估直接遷 DragonflyDB 是否更省維運。</li>
<li><strong>資料其實不能淘汰（被當 source-of-truth）</strong>：那它不是 cache，該走 durable store。AWS 生態下用 <a href="/blog/backend/02-cache-redis/vendors/aws-elasticache/" data-link-title="AWS ElastiCache" data-link-desc="AWS managed Redis / Valkey / Memcached">MemoryDB</a>（Redis-compatible durable），或把正式狀態放回 <a href="/blog/backend/01-database/" data-link-title="模組一：資料庫與持久化" data-link-desc="整理 SQL、transaction、migration 與 repository adapter 的後端實務">database 模組</a>。</li>
</ul>
<h2 id="整合--下一步">整合 / 下一步</h2>
<p>記憶體與淘汰是 Redis 運維的第一層旋鈕，但它跟其他子系統耦合：</p>
<ul>
<li><strong>跟 <a href="/blog/backend/02-cache-redis/vendors/redis/persistence-fork-latency/" data-link-title="Redis 持久化與 fork latency：AOF、RDB 與那一次卡住整個 cluster 的 fork" data-link-desc="Redis 的 RDB save 與 AOF rewrite 都靠一次 fork()，而 fork 在大記憶體實例上會凍結主執行緒數百毫秒、複製分頁讓記憶體逼近翻倍。本文展開 AOF / RDB 的機制與 fsync 取捨、copy-on-write 的記憶體放大、5 個把持久化寫成延遲尖峰與資料遺失的 production 踩坑，以及 cache 場景到底要不要持久化的邊界">persistence / fork latency</a></strong>：fork 期間的 copy-on-write 是 maxmemory headroom 的主要消耗者，記憶體調校跟持久化調校必須一起看。</li>
<li><strong>跟 <a href="/blog/backend/02-cache-redis/ttl-eviction/" data-link-title="2.3 TTL 與 eviction" data-link-desc="整理過期策略、容量控制與熱點資料">TTL 與 eviction 概念</a></strong>：TTL 設計決定哪些 key 帶過期時間，直接影響 <code>volatile-*</code> policy 的淘汰範圍。</li>
<li><strong>跟 <a href="/blog/backend/02-cache-redis/cases/failure-cache-stampede-rollout-regression/" data-link-title="2.C9 反例：快取切換引發 Stampede 回歸" data-link-desc="快取策略切換若缺乏保護，會導致回源壓力與錯誤率連鎖上升。">cache stampede</a></strong>：大量 key 同時被淘汰或同時過期會引發回源雪崩，eviction 調校要跟 TTL jitter / singleflight 一起設計。</li>
<li><strong>跟 <a href="/blog/backend/09-performance-capacity/cases/tubi-elasticache-ml-feature-store/" data-link-title="9.C25 Tubi：從 ScyllaDB 遷到 ElastiCache、ML feature store 達 sub-10ms p99" data-link-desc="Tubi 把 ML 推薦的 feature store 從 ScyllaDB 遷到 ElastiCache for Redis、99 百分位延遲降到 10ms 以下">Tubi 的 cache vs durable 選型</a></strong>：Tubi 把 ML feature store 從 ScyllaDB 遷到 ElastiCache，前提是「feature 可重新計算」——這個判斷決定了 eviction 是可接受的，記憶體調校才有意義。資料若不可重建，問題不在淘汰 policy，在選錯了儲存層。</li>
</ul>
<h2 id="相關連結">相關連結</h2>
<ul>
<li>上游 vendor 頁：<a href="/blog/backend/02-cache-redis/vendors/redis/" data-link-title="Redis" data-link-desc="OSS in-memory data structure store、cache 主流">Redis</a></li>
<li>同 vendor deep article：<a href="/blog/backend/02-cache-redis/vendors/redis/persistence-fork-latency/" data-link-title="Redis 持久化與 fork latency：AOF、RDB 與那一次卡住整個 cluster 的 fork" data-link-desc="Redis 的 RDB save 與 AOF rewrite 都靠一次 fork()，而 fork 在大記憶體實例上會凍結主執行緒數百毫秒、複製分頁讓記憶體逼近翻倍。本文展開 AOF / RDB 的機制與 fsync 取捨、copy-on-write 的記憶體放大、5 個把持久化寫成延遲尖峰與資料遺失的 production 踩坑，以及 cache 場景到底要不要持久化的邊界">persistence 與 fork latency</a>、<a href="/blog/backend/02-cache-redis/vendors/redis/cluster-resharding/" data-link-title="Redis Cluster Re-sharding：source = target，但 topology 重劃的 5 段流程" data-link-desc="Redis cluster re-sharding 是 5 type migration 漏類實證 — source / target 同 cluster、無 schema / paradigm 差、但 16384 slot 重分配是核心；本文涵蓋 4 種 re-sharding driver、slot migration 機制、redis-cli --cluster rebalance / reshard 工具、5 個 production 踩雷（cluster busy / replica lag / client cache stale / cross-slot transaction / monitor gap）">Cluster re-sharding</a></li>
<li>上游概念：<a href="/blog/backend/02-cache-redis/ttl-eviction/" data-link-title="2.3 TTL 與 eviction" data-link-desc="整理過期策略、容量控制與熱點資料">2.3 TTL 與 eviction</a></li>
<li>Methodology：<a href="/blog/posts/vendor-%E6%B7%B1%E5%BA%A6%E6%8A%80%E8%A1%93%E6%96%87%E7%AB%A0%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84%E5%90%8C-vendor-%E7%B3%BB%E5%88%97%E7%9A%84%E9%96%8B%E5%A0%B4%E8%BC%AA%E6%9B%BF%E9%A9%97%E8%AD%89/" data-link-title="Vendor 深度技術文章方法論的演化紀錄：同 vendor 系列的開場輪替驗證" data-link-desc="vendor overview 飽和後要寫單一功能深度文章、需要選題與結構依據時回來。這套方法論的驗證來源與 cadence variant 在高風險場景（同 vendor sub-tool 系列）的實證。">Vendor 深度技術文章寫作方法論</a></li>
</ul>
]]></content:encoded></item><item><title>4.19 Agent memory 分層架構</title><link>https://tarrragon.github.io/blog/llm/04-applications/agent-memory-architecture/</link><pubDate>Tue, 12 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/llm/04-applications/agent-memory-architecture/</guid><description>&lt;p>LLM 本身無狀態 — 每次 &lt;a href="https://tarrragon.github.io/blog/llm/knowledge-cards/forward-pass/" data-link-title="Forward Pass" data-link-desc="input 經過所有 layer 的計算、得到 output 的單向流程；推論跟訓練都會跑、訓練多一個反向階段">forward pass&lt;/a> 從零開始、唯一輸入是 &lt;a href="https://tarrragon.github.io/blog/llm/knowledge-cards/context-window/" data-link-title="Context Window" data-link-desc="模型一次能處理的最大 token 數量：prompt 加生成的總和上限">context window&lt;/a>。但「agent」概念上有跨 session 狀態：使用者偏好、過去任務、累積知識、操作流程。Agent memory 是 harness 層的設計、把這些狀態持久化、按需 inject 到 working context。本章把 memory 分成五個層次、各層的寫入時機、retrieval 設計、失敗模式拆成可操作的工程實務。&lt;/p>
&lt;h2 id="本章目標">本章目標&lt;/h2>
&lt;p>讀完本章後、你應該能：&lt;/p>
&lt;ol>
&lt;li>區分 &lt;a href="https://tarrragon.github.io/blog/llm/knowledge-cards/agent-memory/" data-link-title="Agent Memory" data-link-desc="Agent 在 context window 之外管理長期狀態的設計、五個層次：working / short-term / long-term episodic / semantic / procedural">agent memory&lt;/a> 的五個層次（working / short-term / long-term episodic / semantic / procedural）。&lt;/li>
&lt;li>對自己 agent 場景判斷要哪幾層 memory、不要哪幾層。&lt;/li>
&lt;li>設計 long-term memory 的「何時寫」「何時讀」邏輯。&lt;/li>
&lt;li>認識 memory 的常見失敗模式（drift / PII / 污染）跟對應緩解。&lt;/li>
&lt;/ol>
&lt;h2 id="五個層次的責任劃分">五個層次的責任劃分&lt;/h2>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">[Working memory]：當前 forward pass 的 context window
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl"> - 規模：模型 context（4K-1M token）
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl"> - 範圍：當下這次推論的全部輸入
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl"> - 例：當下 user query + recent tool result + reasoning trace
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl"> ↑ 從這層讀 / 寫到這層
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">[Short-term / session memory]：單一 session 的 scratchpad
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl"> - 規模：一輪對話到一天
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl"> - 範圍：跨多個 turn、但 session 結束就丟
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl"> - 例：本 session 算過的中間結果、tried strategies
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">13&lt;/span>&lt;span class="cl"> ↑ session 結束時可選擇 persist 到 long-term
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">14&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">15&lt;/span>&lt;span class="cl">[Long-term episodic memory]：跨 session 的「事件」
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">16&lt;/span>&lt;span class="cl"> - 規模：永久（直到主動刪除）
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">17&lt;/span>&lt;span class="cl"> - 範圍：跨所有 session、按時間順序
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">18&lt;/span>&lt;span class="cl"> - 例：「上週解過這個 race condition」「alice 上個月問過 X」
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">19&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">20&lt;/span>&lt;span class="cl">[Long-term semantic memory]：跨 session 的「事實 / 知識」
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">21&lt;/span>&lt;span class="cl"> - 規模：永久
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">22&lt;/span>&lt;span class="cl"> - 範圍：跨所有 session、按主題索引
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">23&lt;/span>&lt;span class="cl"> - 例：「user 偏好 markdown 輸出」「專案用 React 18」「team 不用 Tailwind」
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">24&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">25&lt;/span>&lt;span class="cl">[Long-term procedural memory]：跨 session 的「流程 / 技能」
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">26&lt;/span>&lt;span class="cl"> - 規模：永久
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">27&lt;/span>&lt;span class="cl"> - 範圍：可重複使用的 known-good 程序
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">28&lt;/span>&lt;span class="cl"> - 例：「跑測試前先 npm install」「commit 前要 lint」「deploy 前要 dry-run」&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>跟人類認知科學的對應：working ≈ 短期工作記憶、episodic ≈ 「我昨天去哪裡了」、semantic ≈ 「巴黎是法國首都」、procedural ≈ 「騎腳踏車的肌肉記憶」。&lt;/p></description><content:encoded><![CDATA[<p>LLM 本身無狀態 — 每次 <a href="/blog/llm/knowledge-cards/forward-pass/" data-link-title="Forward Pass" data-link-desc="input 經過所有 layer 的計算、得到 output 的單向流程；推論跟訓練都會跑、訓練多一個反向階段">forward pass</a> 從零開始、唯一輸入是 <a href="/blog/llm/knowledge-cards/context-window/" data-link-title="Context Window" data-link-desc="模型一次能處理的最大 token 數量：prompt 加生成的總和上限">context window</a>。但「agent」概念上有跨 session 狀態：使用者偏好、過去任務、累積知識、操作流程。Agent memory 是 harness 層的設計、把這些狀態持久化、按需 inject 到 working context。本章把 memory 分成五個層次、各層的寫入時機、retrieval 設計、失敗模式拆成可操作的工程實務。</p>
<h2 id="本章目標">本章目標</h2>
<p>讀完本章後、你應該能：</p>
<ol>
<li>區分 <a href="/blog/llm/knowledge-cards/agent-memory/" data-link-title="Agent Memory" data-link-desc="Agent 在 context window 之外管理長期狀態的設計、五個層次：working / short-term / long-term episodic / semantic / procedural">agent memory</a> 的五個層次（working / short-term / long-term episodic / semantic / procedural）。</li>
<li>對自己 agent 場景判斷要哪幾層 memory、不要哪幾層。</li>
<li>設計 long-term memory 的「何時寫」「何時讀」邏輯。</li>
<li>認識 memory 的常見失敗模式（drift / PII / 污染）跟對應緩解。</li>
</ol>
<h2 id="五個層次的責任劃分">五個層次的責任劃分</h2>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln"> 1</span><span class="cl">[Working memory]：當前 forward pass 的 context window
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">   - 規模：模型 context（4K-1M token）
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">   - 範圍：當下這次推論的全部輸入
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">   - 例：當下 user query + recent tool result + reasoning trace
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">       ↑ 從這層讀 / 寫到這層
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">[Short-term / session memory]：單一 session 的 scratchpad
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">   - 規模：一輪對話到一天
</span></span><span class="line"><span class="ln">10</span><span class="cl">   - 範圍：跨多個 turn、但 session 結束就丟
</span></span><span class="line"><span class="ln">11</span><span class="cl">   - 例：本 session 算過的中間結果、tried strategies
</span></span><span class="line"><span class="ln">12</span><span class="cl">
</span></span><span class="line"><span class="ln">13</span><span class="cl">       ↑ session 結束時可選擇 persist 到 long-term
</span></span><span class="line"><span class="ln">14</span><span class="cl">
</span></span><span class="line"><span class="ln">15</span><span class="cl">[Long-term episodic memory]：跨 session 的「事件」
</span></span><span class="line"><span class="ln">16</span><span class="cl">   - 規模：永久（直到主動刪除）
</span></span><span class="line"><span class="ln">17</span><span class="cl">   - 範圍：跨所有 session、按時間順序
</span></span><span class="line"><span class="ln">18</span><span class="cl">   - 例：「上週解過這個 race condition」「alice 上個月問過 X」
</span></span><span class="line"><span class="ln">19</span><span class="cl">
</span></span><span class="line"><span class="ln">20</span><span class="cl">[Long-term semantic memory]：跨 session 的「事實 / 知識」
</span></span><span class="line"><span class="ln">21</span><span class="cl">   - 規模：永久
</span></span><span class="line"><span class="ln">22</span><span class="cl">   - 範圍：跨所有 session、按主題索引
</span></span><span class="line"><span class="ln">23</span><span class="cl">   - 例：「user 偏好 markdown 輸出」「專案用 React 18」「team 不用 Tailwind」
</span></span><span class="line"><span class="ln">24</span><span class="cl">
</span></span><span class="line"><span class="ln">25</span><span class="cl">[Long-term procedural memory]：跨 session 的「流程 / 技能」
</span></span><span class="line"><span class="ln">26</span><span class="cl">   - 規模：永久
</span></span><span class="line"><span class="ln">27</span><span class="cl">   - 範圍：可重複使用的 known-good 程序
</span></span><span class="line"><span class="ln">28</span><span class="cl">   - 例：「跑測試前先 npm install」「commit 前要 lint」「deploy 前要 dry-run」</span></span></code></pre></div><p>跟人類認知科學的對應：working ≈ 短期工作記憶、episodic ≈ 「我昨天去哪裡了」、semantic ≈ 「巴黎是法國首都」、procedural ≈ 「騎腳踏車的肌肉記憶」。</p>
<h2 id="不是每個-agent-都要五個層次都用">不是每個 agent 都要五個層次都用</h2>
<p>選擇看用例：</p>
<table>
  <thead>
      <tr>
          <th>用例</th>
          <th>Working</th>
          <th>Session</th>
          <th>Episodic</th>
          <th>Semantic</th>
          <th>Procedural</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Autocomplete（單行補完）</td>
          <td>需要</td>
          <td>不需要</td>
          <td>不需要</td>
          <td>不需要</td>
          <td>不需要</td>
      </tr>
      <tr>
          <td>Single-turn Q&amp;A</td>
          <td>需要</td>
          <td>不需要</td>
          <td>不需要</td>
          <td>不需要</td>
          <td>不需要</td>
      </tr>
      <tr>
          <td>Chat IDE assistant（短對話）</td>
          <td>需要</td>
          <td>需要</td>
          <td>不需要</td>
          <td>不需要</td>
          <td>不需要</td>
      </tr>
      <tr>
          <td>Chat IDE assistant（長期使用）</td>
          <td>需要</td>
          <td>需要</td>
          <td>可選</td>
          <td>需要</td>
          <td>可選</td>
      </tr>
      <tr>
          <td>長期 coding agent（持續同 codebase）</td>
          <td>需要</td>
          <td>需要</td>
          <td>需要</td>
          <td>需要</td>
          <td>需要</td>
      </tr>
      <tr>
          <td>Multi-session research agent</td>
          <td>需要</td>
          <td>需要</td>
          <td>需要</td>
          <td>需要</td>
          <td>需要</td>
      </tr>
  </tbody>
</table>
<p>實務啟示：從「最少 memory」開始、有具體 trigger 才加。memory 不是越多越好、每加一層都增加複雜度跟失敗面。</p>
<h2 id="long-term-memory-的寫入時機">Long-term memory 的寫入時機</h2>
<p><strong>何時寫</strong>是設計核心、影響 memory 的品質跟成本。三種主流模式：</p>
<h3 id="1-每-turn-寫auto-write">1. 每 turn 寫（Auto-write）</h3>
<p>每個對話 turn 結束都寫一條 memory。實作簡單但 memory 變垃圾場 — 太多瑣碎內容、retrieval 時混淆 signal。</p>
<p><strong>適合</strong>：實驗階段、想看 memory 怎麼累積
<strong>不適合</strong>：production、長期使用</p>
<h3 id="2-任務結束寫task-end-write">2. 任務結束寫（Task-end write）</h3>
<p>每個明確「任務」（如「修完 bug」「寫完 feature」）結束時、寫一條 episodic / semantic memory 摘要。</p>
<p>實作：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">任務開始 → working memory 進入「task mode」
</span></span><span class="line"><span class="ln">2</span><span class="cl">   ↓ 多 turn 累積 session scratchpad
</span></span><span class="line"><span class="ln">3</span><span class="cl">任務結束（user 說「好了」/ test 通過 / commit done）
</span></span><span class="line"><span class="ln">4</span><span class="cl">   ↓ trigger memory write
</span></span><span class="line"><span class="ln">5</span><span class="cl">LLM call：「請從本 session 提取值得記得的 episodic / semantic / procedural memory」
</span></span><span class="line"><span class="ln">6</span><span class="cl">   ↓ 結構化輸出
</span></span><span class="line"><span class="ln">7</span><span class="cl">寫進 long-term store</span></span></code></pre></div><p><strong>適合</strong>：production agent、明確任務邊界
<strong>不適合</strong>：開放式對話、無明確任務終點</p>
<h3 id="3-主動觸發寫reflection--consolidation">3. 主動觸發寫（Reflection / consolidation）</h3>
<p>定期（每 N turn / 每天）跑「memory consolidation」step、LLM 自己決定該寫什麼。借鑒人類睡眠時 memory consolidation 的研究。</p>
<p><strong>適合</strong>：長 running agent、有明確 idle 時間
<strong>不適合</strong>：低 cost 場景（consolidation 額外 LLM call 是常駐成本）</p>
<p>混用：production 多用「task-end write」為主 + 偶爾 reflection 做 consolidation。</p>
<h2 id="long-term-memory-的-retrieval">Long-term memory 的 retrieval</h2>
<p><strong>何時讀</strong>也是設計核心。三種主流模式：</p>
<h3 id="1-inject-on-startup">1. Inject-on-startup</h3>
<p>把 long-term memory 在 session / agent 啟動時一次塞進 system prompt。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">System prompt:
</span></span><span class="line"><span class="ln">2</span><span class="cl">  &#34;你是 coding assistant、user alice。
</span></span><span class="line"><span class="ln">3</span><span class="cl">   semantic memory: {markdown 偏好、React 18、Python 3.11、...}
</span></span><span class="line"><span class="ln">4</span><span class="cl">   procedural memory: {npm install before test、lint before commit、...}&#34;</span></span></code></pre></div><p><strong>適合</strong>：memory 量小（&lt; 1K token）、相對穩定
<strong>不適合</strong>：memory 多、變動快、retrieval 不準</p>
<h3 id="2-retrieval-on-demand">2. Retrieval-on-demand</h3>
<p>每次 user query 來、用 <a href="/blog/llm/04-applications/embedding-model-internals/" data-link-title="4.12 Embedding model 內部：訓練、選型、in-domain fine-tune" data-link-desc="Embedding model 怎麼訓練（contrastive learning &#43; hard negative mining）、怎麼挑（MTEB / 大小 / domain）、何時該自己 fine-tune">embedding similarity</a> 從 vector store retrieve 相關 memory、塞進 context。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">User query → embed → cosine similarity vs memory vectors → top-K → inject</span></span></code></pre></div><p><strong>適合</strong>：memory 量大、跨主題、需要動態
<strong>不適合</strong>：高頻 / 低 latency 要求（retrieval overhead）</p>
<h3 id="3-hybrid混合">3. Hybrid（混合）</h3>
<p>Procedural / semantic（穩定）→ inject-on-startup；episodic（動態）→ retrieval-on-demand。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">Session 啟動：
</span></span><span class="line"><span class="ln">2</span><span class="cl">  inject procedural + semantic（小、穩定）
</span></span><span class="line"><span class="ln">3</span><span class="cl">
</span></span><span class="line"><span class="ln">4</span><span class="cl">每 user query：
</span></span><span class="line"><span class="ln">5</span><span class="cl">  retrieve top-K episodic（動態）+ inject</span></span></code></pre></div><p>實務 production 多採 hybrid。</p>
<h2 id="跟-rag-的邊界">跟 <a href="/blog/llm/knowledge-cards/rag/" data-link-title="RAG" data-link-desc="Retrieval-Augmented Generation：動態外掛知識給 LLM、繞開模型參數記憶的靜態限制">RAG</a> 的邊界</h2>
<p>Agent memory 跟 RAG 容易混淆、實際上是不同概念：</p>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>RAG</th>
          <th>Long-term agent memory</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>主要內容</td>
          <td>外部知識庫（docs、wiki、codebase）</td>
          <td>Agent 跟特定 user 的互動歷史</td>
      </tr>
      <tr>
          <td>Per-user？</td>
          <td>通常通用</td>
          <td>Per-user / per-session</td>
      </tr>
      <tr>
          <td>寫入時機</td>
          <td>Build time / ingestion pipeline</td>
          <td>Runtime（agent 自己決定何時寫）</td>
      </tr>
      <tr>
          <td>變動頻率</td>
          <td>較慢（doc 更新）</td>
          <td>快（每 session 都可能變）</td>
      </tr>
      <tr>
          <td>是否含「事件」</td>
          <td>否（純知識）</td>
          <td>Episodic memory 是事件</td>
      </tr>
  </tbody>
</table>
<p>但兩者實作層常共享：vector store / embedding model / retrieval logic 可重用。設計上：</p>
<ul>
<li><strong>如果讀者問「跟『過去聊過的事』有關」→ memory</strong></li>
<li><strong>如果讀者問「跟『某個固定知識』有關」→ RAG</strong></li>
<li><strong>同一個 query 兩者都要 → hybrid retrieval、結果合併</strong></li>
</ul>
<h2 id="失敗模式">失敗模式</h2>
<h3 id="1-memory-drift記憶過時">1. Memory drift（記憶過時）</h3>
<p>舊 memory 寫的內容不再正確、但仍被 retrieve、agent 用過時資訊。</p>
<p><strong>例</strong>：兩個月前寫 memory「user 偏好 React class component」、user 已換 hooks、agent 仍寫 class component。</p>
<p><strong>緩解</strong>：</p>
<ul>
<li>Memory 加 timestamp、retrieval 時加 time decay weighting</li>
<li>定期 consolidation：LLM 跑一遍判斷哪些 memory 過時</li>
<li>Procedural / semantic memory 跑「validation step」：當前對話是否仍 align、不 align 就 mark stale</li>
</ul>
<h3 id="2-pii-寫入">2. PII 寫入</h3>
<p>User 不知情下、agent 把 PII（email、phone、社群 ID）寫進 long-term memory、跨 session retrieve 出來、可能洩漏。</p>
<p><strong>緩解</strong>：</p>
<ul>
<li>Memory write 前過 PII detection（regex 或專門模型）</li>
<li>Memory store 加 encryption-at-rest</li>
<li>User 可看 / 編輯 / 刪除自己 memory（GDPR / 隱私法規要求）</li>
<li>跟 <a href="/blog/llm/06-security/cross-cloud-local-data-boundary/" data-link-title="6.4 跨雲端 / 本地的資料邊界" data-link-desc="個人 dev 場景下混用雲端 LLM 跟本地 LLM 時的 prompt 洩漏點：Continue.dev 多 provider 設定、隱私資料流、按敏感度分流的判讀">6.4 跨雲端資料邊界</a> 結合判讀</li>
</ul>
<h3 id="3-context-污染">3. Context 污染</h3>
<p>不相關 memory 被 retrieve 進 working memory、模型把 irrelevant 內容當 signal、輸出飄。</p>
<p><strong>例</strong>：user 問 React 問題、retrieve 出兩個月前的 Vue 經驗、模型混淆。</p>
<p><strong>緩解</strong>：</p>
<ul>
<li>Retrieval 加 similarity threshold（&lt; 0.7 不 inject）</li>
<li>Memory 加 metadata（topic / project / language）、retrieval 加 filter</li>
<li>Inject 後加 explicit framing：「以下是過去相關 memory、僅供參考、若跟當前問題不符請忽略」</li>
</ul>
<h3 id="4-memory-跟-hallucination-互相-boost">4. Memory 跟 hallucination 互相 boost</h3>
<p><a href="/blog/llm/knowledge-cards/hallucination/" data-link-title="Hallucination" data-link-desc="LLM 生成內容看起來合理但事實錯誤、引用不存在的來源、虛構不存在的 entity 的現象">Hallucination</a> 寫進 memory、變成「事實」、後續 retrieve 強化 hallucination、agent 越來越相信錯誤內容。</p>
<p><strong>緩解</strong>：</p>
<ul>
<li>Memory write 前要求 LLM 標「不確定」flag、retrieval 時 deprioritize</li>
<li>定期 ground truth validation（如連結 memory 到實際檔案、檔案變了 memory 失效）</li>
<li>Critical memory 要 user 確認才寫入</li>
</ul>
<h3 id="5-跨-user-memory-污染">5. 跨 user memory 污染</h3>
<p>Production 多 user 場景、memory store 沒做 user isolation、A user 的 memory 流到 B user。</p>
<p><strong>緩解</strong>：</p>
<ul>
<li>Memory store schema 強制 user_id 索引</li>
<li>Retrieval query 必加 user_id filter</li>
<li>跟 <a href="/blog/llm/06-security/routing-to-production-security/" data-link-title="6.5 跨進 production 的 routing 中樞" data-link-desc="個人 dev → 團隊 → production LLM 服務的三層演化、跟 backend/07 對應卡片的 routing 清單">6.5 routing-to-production</a> 的多租戶 isolation 結合</li>
</ul>
<h2 id="主流實作">主流實作</h2>
<table>
  <thead>
      <tr>
          <th>工具 / framework</th>
          <th>特色</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Mem0</td>
          <td>開源、五層 memory framework、retrieval-on-demand</td>
      </tr>
      <tr>
          <td>Letta（前 MemGPT）</td>
          <td>LLM-managed memory hierarchy、自動 page in/out</td>
      </tr>
      <tr>
          <td>LangGraph memory</td>
          <td>LangChain 系、跟 graph workflow 整合</td>
      </tr>
      <tr>
          <td>Zep</td>
          <td>雲端 memory service、含 PII detection</td>
      </tr>
      <tr>
          <td>Self-implemented（DIY）</td>
          <td>多數 production 自寫、用 vector store + metadata</td>
      </tr>
  </tbody>
</table>
<p>判讀：用既有 framework vs 自己寫、取決於 memory 邏輯複雜度。簡單 case（per-user semantic preferences）用 DIY 即可；多層 memory + consolidation + GDPR 合規要 framework / SaaS。</p>
<h2 id="跟-coding-agent-的整合">跟 Coding agent 的整合</h2>
<p>Coding agent 場景的 memory 案例：</p>
<table>
  <thead>
      <tr>
          <th>Memory 類型</th>
          <th>內容例子</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Semantic</td>
          <td>「專案用 TypeScript strict mode」「team 不用 anonymous default export」</td>
      </tr>
      <tr>
          <td>Procedural</td>
          <td>「跑測試 = <code>npm test</code>」「commit 前 <code>npm run lint</code>」</td>
      </tr>
      <tr>
          <td>Episodic</td>
          <td>「上週解過 race condition 在 user_session.ts」「alice 的 retry 邏輯偏好」</td>
      </tr>
  </tbody>
</table>
<p>跟 <a href="/blog/llm/04-applications/coding-agent-harness/" data-link-title="4.17 Coding agent harness：scaffold / context engineering / subagent" data-link-desc="Coding agent 的內部設計：scaffold vs harness 分層、context budget 25% 規則、subagent 拓樸、跟 Claude Code / Cursor / Aider 的 mapping">4.17 coding agent harness</a> 的關係：</p>
<ul>
<li>Procedural memory 編進 <a href="/blog/llm/knowledge-cards/scaffold-vs-harness/" data-link-title="Scaffold vs Harness" data-link-desc="Coding agent 的兩個工程層次：scaffold 是建構時靜態結構、harness 是 runtime 的 tool dispatch &#43; context management &#43; safety">scaffold</a> 的 system prompt 或 skill registry</li>
<li>Semantic memory 可 inject-on-startup 或 retrieval-on-demand</li>
<li>Episodic memory 用 retrieval-on-demand、跟 <a href="/blog/llm/04-applications/rag-principles/" data-link-title="4.1 RAG 原理：retrieval &#43; augmentation 模式" data-link-desc="為什麼模型需要外掛知識、語意相似 vs 字面相似、chunking 的本質取捨、retrieval 失敗的根本原因">RAG</a> 共享 infrastructure</li>
</ul>
<h2 id="何時過時--何時不過時">何時過時 / 何時不過時</h2>
<p><strong>不會過時的部分</strong>：</p>
<ul>
<li>五層 memory 分類（working / session / episodic / semantic / procedural）</li>
<li>「不是每個 agent 都要五層都用」的選擇框架</li>
<li>寫入時機的三種模式（auto / task-end / reflection）</li>
<li>Retrieval 的三種模式（inject / retrieval / hybrid）</li>
<li>五個失敗模式分類</li>
</ul>
<p><strong>會變的部分</strong>：</p>
<ul>
<li>具體 framework（Mem0 / Letta / LangGraph）的 API</li>
<li>LLM-managed memory 的具體實作（如 MemGPT 風格的 paging）</li>
<li>Memory consolidation 的最佳實踐</li>
<li>整合 LLM 跟 vector store / DB 的最佳方式</li>
</ul>
<h2 id="下一章">下一章</h2>
<p>下一章：<a href="/blog/llm/04-applications/llm-tracing-and-observability/" data-link-title="4.20 LLM tracing 與 observability" data-link-desc="OpenTelemetry GenAI semantic conventions、結構化 span 設計、cost / latency 監控、failure debug 流程、跟 LLM-as-judge eval 的串接">4.20 LLM tracing 與 observability</a>、看 production debug 跟 cost 監控的工具層。</p>
]]></content:encoded></item><item><title>MySQL Cross-buffer Memory Contention</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/mysql/cross-buffer-memory-contention/</link><pubDate>Fri, 22 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/mysql/cross-buffer-memory-contention/</guid><description>&lt;p>MySQL cross-buffer memory contention 的核心責任是把 MySQL memory tuning 從單一 buffer pool 參數擴展到整體記憶體競爭。InnoDB buffer pool、redo log buffer、sort buffer、join buffer、tmp table、thread stack、connection memory、OS page cache 與 container limit 會共同決定 latency 與 OOM 風險。&lt;/p>
&lt;p>本文的判讀錨點是：MySQL memory 問題常來自&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/per-connection-memory/" data-link-title="Per-Connection Memory" data-link-desc="說明每條連線或每個操作的記憶體用量如何隨並發數放大">「每連線 / 每操作」記憶體&lt;/a>乘上 concurrency，而非只來自全域 buffer pool。調大單一 buffer 前，要先看 workload 與同時執行的 query。&lt;/p>
&lt;h2 id="memory-surfaces">Memory Surfaces&lt;/h2>
&lt;p>Memory surfaces 的核心責任是列出會互相競爭的記憶體來源。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>Surface&lt;/th>
 &lt;th>類型&lt;/th>
 &lt;th>風險&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>InnoDB buffer pool&lt;/td>
 &lt;td>global&lt;/td>
 &lt;td>太小造成 read I/O，太大壓縮 OS 空間&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Redo log buffer&lt;/td>
 &lt;td>global&lt;/td>
 &lt;td>大交易 / burst write 需要審查&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Sort buffer&lt;/td>
 &lt;td>per session / operation&lt;/td>
 &lt;td>concurrent sort 放大 memory&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Join buffer&lt;/td>
 &lt;td>per session / join&lt;/td>
 &lt;td>missing index 時放大&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Temp table&lt;/td>
 &lt;td>memory / disk&lt;/td>
 &lt;td>group / sort / derived table&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Connection overhead&lt;/td>
 &lt;td>per connection&lt;/td>
 &lt;td>connection storm / thread memory&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>OS page cache&lt;/td>
 &lt;td>system&lt;/td>
 &lt;td>file、backup、binlog、tmp&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>Per-session buffer 是最容易誤調的項目。把 sort / join buffer 全域調大，會在高 concurrency 下造成 memory spike。&lt;/p>
&lt;h2 id="contention-signals">Contention Signals&lt;/h2>
&lt;p>Contention signals 的核心責任是把 memory pressure 從 symptom 轉成可排查訊號。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>Signal&lt;/th>
 &lt;th>意義&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>OOM / container restart&lt;/td>
 &lt;td>total memory 超出限制&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>swap activity&lt;/td>
 &lt;td>memory pressure 已影響 latency&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Created_tmp_disk_tables 增加&lt;/td>
 &lt;td>memory temp table 不足或 query 太大&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Sort_merge_passes 增加&lt;/td>
 &lt;td>sort memory / query shape 問題&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Buffer pool hit rate 下降&lt;/td>
 &lt;td>working set / query pattern 問題&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Threads_connected 高&lt;/td>
 &lt;td>per-connection memory 放大&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>Signal 要和 query workload 對照。Temp table 與 sort 問題通常需要 query rewrite、index 或報表隔離，而非只調 memory。&lt;/p>
&lt;h2 id="tuning-order">Tuning Order&lt;/h2>
&lt;p>Tuning order 的核心責任是建立安全調整順序。&lt;/p></description><content:encoded><![CDATA[<p>MySQL cross-buffer memory contention 的核心責任是把 MySQL memory tuning 從單一 buffer pool 參數擴展到整體記憶體競爭。InnoDB buffer pool、redo log buffer、sort buffer、join buffer、tmp table、thread stack、connection memory、OS page cache 與 container limit 會共同決定 latency 與 OOM 風險。</p>
<p>本文的判讀錨點是：MySQL memory 問題常來自<a href="/blog/backend/knowledge-cards/per-connection-memory/" data-link-title="Per-Connection Memory" data-link-desc="說明每條連線或每個操作的記憶體用量如何隨並發數放大">「每連線 / 每操作」記憶體</a>乘上 concurrency，而非只來自全域 buffer pool。調大單一 buffer 前，要先看 workload 與同時執行的 query。</p>
<h2 id="memory-surfaces">Memory Surfaces</h2>
<p>Memory surfaces 的核心責任是列出會互相競爭的記憶體來源。</p>
<table>
  <thead>
      <tr>
          <th>Surface</th>
          <th>類型</th>
          <th>風險</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>InnoDB buffer pool</td>
          <td>global</td>
          <td>太小造成 read I/O，太大壓縮 OS 空間</td>
      </tr>
      <tr>
          <td>Redo log buffer</td>
          <td>global</td>
          <td>大交易 / burst write 需要審查</td>
      </tr>
      <tr>
          <td>Sort buffer</td>
          <td>per session / operation</td>
          <td>concurrent sort 放大 memory</td>
      </tr>
      <tr>
          <td>Join buffer</td>
          <td>per session / join</td>
          <td>missing index 時放大</td>
      </tr>
      <tr>
          <td>Temp table</td>
          <td>memory / disk</td>
          <td>group / sort / derived table</td>
      </tr>
      <tr>
          <td>Connection overhead</td>
          <td>per connection</td>
          <td>connection storm / thread memory</td>
      </tr>
      <tr>
          <td>OS page cache</td>
          <td>system</td>
          <td>file、backup、binlog、tmp</td>
      </tr>
  </tbody>
</table>
<p>Per-session buffer 是最容易誤調的項目。把 sort / join buffer 全域調大，會在高 concurrency 下造成 memory spike。</p>
<h2 id="contention-signals">Contention Signals</h2>
<p>Contention signals 的核心責任是把 memory pressure 從 symptom 轉成可排查訊號。</p>
<table>
  <thead>
      <tr>
          <th>Signal</th>
          <th>意義</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>OOM / container restart</td>
          <td>total memory 超出限制</td>
      </tr>
      <tr>
          <td>swap activity</td>
          <td>memory pressure 已影響 latency</td>
      </tr>
      <tr>
          <td>Created_tmp_disk_tables 增加</td>
          <td>memory temp table 不足或 query 太大</td>
      </tr>
      <tr>
          <td>Sort_merge_passes 增加</td>
          <td>sort memory / query shape 問題</td>
      </tr>
      <tr>
          <td>Buffer pool hit rate 下降</td>
          <td>working set / query pattern 問題</td>
      </tr>
      <tr>
          <td>Threads_connected 高</td>
          <td>per-connection memory 放大</td>
      </tr>
  </tbody>
</table>
<p>Signal 要和 query workload 對照。Temp table 與 sort 問題通常需要 query rewrite、index 或報表隔離，而非只調 memory。</p>
<h2 id="tuning-order">Tuning Order</h2>
<p>Tuning order 的核心責任是建立安全調整順序。</p>
<ol>
<li>先確認 host / container memory limit。</li>
<li>設定 InnoDB buffer pool baseline。</li>
<li>控制 max connections 與 application pool。</li>
<li>用 top query 找 sort / join / temp table 來源。</li>
<li>對特定 session / workload 調 buffer，而非全域放大。</li>
<li>將 analytics / reporting 移到 replica 或 OLAP。</li>
</ol>
<p>這個順序讓全域 memory 先穩定，再處理 query 層問題。若反過來先調大 per-session buffer，壓力會在尖峰流量時爆發。</p>
<h2 id="query-patterns">Query Patterns</h2>
<p>Query patterns 的核心責任是找出 memory heavy 查詢。</p>
<table>
  <thead>
      <tr>
          <th>Pattern</th>
          <th>Memory 風險</th>
          <th>修正方向</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Large sort</td>
          <td>sort buffer / temp table</td>
          <td>index order、limit、pagination</td>
      </tr>
      <tr>
          <td>Missing join index</td>
          <td>join buffer 放大</td>
          <td>補 index、改 join order</td>
      </tr>
      <tr>
          <td>Big GROUP BY</td>
          <td>tmp table / disk spill</td>
          <td>pre-aggregate、OLAP、covering index</td>
      </tr>
      <tr>
          <td>Large transaction</td>
          <td>undo / lock / memory</td>
          <td>batch、縮短 transaction</td>
      </tr>
      <tr>
          <td>Many idle sessions</td>
          <td>connection memory</td>
          <td>pooler、timeout、max connection</td>
      </tr>
  </tbody>
</table>
<p>Memory tuning 要服務 query design。若 query 本身無界，memory 只會把問題延後到更大資料量。</p>
<h2 id="runbook">Runbook</h2>
<p>Runbook 的核心責任是把 memory incident 分流。</p>
<table>
  <thead>
      <tr>
          <th>Step</th>
          <th>操作</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Confirm pressure</td>
          <td>OS memory、swap、OOM、MySQL status</td>
      </tr>
      <tr>
          <td>Identify workload</td>
          <td>processlist、performance schema、top SQL</td>
      </tr>
      <tr>
          <td>Reduce concurrency</td>
          <td>限流、停報表、降 background job</td>
      </tr>
      <tr>
          <td>Protect OLTP</td>
          <td>kill heavy query、切 read replica</td>
      </tr>
      <tr>
          <td>Tune safely</td>
          <td>session-level buffer、index、query</td>
      </tr>
      <tr>
          <td>Retrospective</td>
          <td>pool size、query guard、dashboard</td>
      </tr>
  </tbody>
</table>
<p>OOM 後要保存 evidence：memory limit、MySQL variables、Threads_connected、top queries、tmp table counters、container restart time。</p>
<h2 id="下一步路由">下一步路由</h2>
<p>Cross-buffer memory contention 完成後，InnoDB 基礎讀 <a href="../innodb-tuning/">InnoDB Tuning</a>；query 層讀 <a href="../query-optimization/">Query Optimization</a>；lock 與 transaction 壓力讀 <a href="../lock-contention/">Lock Contention</a>。</p>
]]></content:encoded></item></channel></rss>