<?xml version="1.0" encoding="UTF-8"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en">
	<title>0xfa003f</title>
	<link href="https://trinkle23897.pages.dev/atom.xml" rel="self" type="application/atom+xml"/>
	<link href="https://trinkle23897.pages.dev"/>
	<generator uri="https://www.getzola.org/">Zola</generator>
	<updated>2025-05-04T00:00:00+00:00</updated>
	<id>https://trinkle23897.pages.dev/atom.xml</id>
	<entry xml:lang="en">
		<title>Transformer学习笔记：自注意力与多头注意力</title>
		<published>2025-05-04T00:00:00+00:00</published>
		<updated>2025-05-04T00:00:00+00:00</updated>
		<link href="https://trinkle23897.pages.dev/transformer-architecture/"/>
		<link rel="alternate" href="https://trinkle23897.pages.dev/transformer-architecture/" type="text/html"/>
		<id>https://trinkle23897.pages.dev/transformer-architecture/</id>
		<content type="html">&lt;h2 id=&quot;xie-zai-qian-mian&quot;&gt;写在前面&lt;a class=&quot;zola-anchor&quot; href=&quot;#xie-zai-qian-mian&quot; aria-label=&quot;Anchor link for: xie-zai-qian-mian&quot;&gt;&lt;i class=&quot;fas fa-link&quot;&gt;&lt;&#x2F;i&gt;&lt;&#x2F;a&gt; 
&lt;&#x2F;h2&gt;
&lt;p&gt;我一直想搞明白&quot;大模型到底是怎么工作的&quot;。结果一搜资料，铺天盖地都是 Transformer、Attention、QKV 这些词。硬着头皮去读那篇 &lt;strong&gt;Attention Is All You Need&lt;&#x2F;strong&gt;，说实话第一遍读下来是懵的。&lt;&#x2F;p&gt;
&lt;p&gt;后来花了几个周末，找了不少博客、视频、代码实现，才慢慢把整个拼图拼起来。这篇文章就是我自己的学习笔记，从我最开始困惑的地方写起，希望能帮到同样在自学的人。&lt;&#x2F;p&gt;
&lt;hr &#x2F;&gt;
&lt;h2 id=&quot;yi-wei-shen-me-xu-yao-zhu-yi-li-ji-zhi&quot;&gt;一、为什么需要注意力机制&lt;a class=&quot;zola-anchor&quot; href=&quot;#yi-wei-shen-me-xu-yao-zhu-yi-li-ji-zhi&quot; aria-label=&quot;Anchor link for: yi-wei-shen-me-xu-yao-zhu-yi-li-ji-zhi&quot;&gt;&lt;i class=&quot;fas fa-link&quot;&gt;&lt;&#x2F;i&gt;&lt;&#x2F;a&gt; 
&lt;&#x2F;h2&gt;
&lt;p&gt;在 Transformer 之前，做序列任务的主流方案是 RNN 和 LSTM。它们在处理&quot;一句话&quot;这样的序列数据时，是一个词一个词按顺序读的：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;我 -&amp;gt; 爱 -&amp;gt; 你
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;每一步的隐藏状态都由上一步决定，数据流是串行的。这就带来两个要命的问题：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;长距离依赖难搞&lt;&#x2F;strong&gt;：信息一步步往后传，越远越容易丢失。比如句子&quot;我在北京待了十年，但还是不习惯这里的__&quot;，要预测&quot;这里&quot;指的是&quot;北京&quot;，中间隔了十几个词，RNN 往往就忘了。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;没法并行训练&lt;&#x2F;strong&gt;：第 3 个词必须等第 2 个词算完，GPU 的优势发挥不出来。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;那怎么办？研究者想到一个很自然的思路：让模型在处理某个词的时候，能&lt;strong&gt;直接看到&lt;&#x2F;strong&gt;句子中所有其他词，然后自己决定关注谁。&lt;&#x2F;p&gt;
&lt;p&gt;这就是 Attention。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;qkv-zen-me-li-jie&quot;&gt;QKV 怎么理解&lt;a class=&quot;zola-anchor&quot; href=&quot;#qkv-zen-me-li-jie&quot; aria-label=&quot;Anchor link for: qkv-zen-me-li-jie&quot;&gt;&lt;i class=&quot;fas fa-link&quot;&gt;&lt;&#x2F;i&gt;&lt;&#x2F;a&gt; 
&lt;&#x2F;h3&gt;
&lt;p&gt;注意力机制的核心公式长这样：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;Attention(Q, K, V) = softmax(QK^T &amp;#x2F; √d_k) V
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;我第一次看到 Q、K、V 的时候在想：这搞什么飞机？后来看到一个类比觉得特别好——&lt;strong&gt;图书馆查书&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Q（Query）&lt;&#x2F;strong&gt;：你想查的书，好比&quot;机器学习的入门书籍&quot;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;K（Key）&lt;&#x2F;strong&gt;：每本书的标签，好比&quot;机器学习&quot;、&quot;Python&quot;、&quot;深度学习&quot;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;V（Value）&lt;&#x2F;strong&gt;：书的实际内容&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;你拿着 Q 去图书馆，挨个跟每本书的 K 比对相似度（点积），匹配度高的书就多看几眼（权重高），匹配度低的就略过。最后你对这个主题的理解（输出），是所有书内容的一个加权综合。&lt;&#x2F;p&gt;
&lt;p&gt;对应到文本中，如果句子是&quot;它站在银行门口&quot;，那么&quot;它&quot;作为 Q，去匹配&quot;银行&quot;、&quot;门口&quot;、&quot;站&quot;这些 K。匹配度高说明它们关系密切，最终&quot;它&quot;的向量表示里就融入了&quot;银行&quot;的信息。&lt;&#x2F;p&gt;
&lt;p&gt;这里的关键是 Q、K、V 不是输入本身，而是输入经过三个不同的线性变换得到的：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;python&quot; class=&quot;language-python &quot;&gt;&lt;code class=&quot;language-python&quot; data-lang=&quot;python&quot;&gt;# 这三个 W 是模型要学习的参数，把输入映射到不同的&amp;quot;语义空间&amp;quot;
# 类比一下：Q 是&amp;quot;我想找什么&amp;quot;，K 是&amp;quot;我有什么&amp;quot;，V 是&amp;quot;我实际的内容&amp;quot;
Q = X @ W_q
K = X @ W_k
V = X @ W_v
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;W_Q、W_K、W_V 是模型学习出来的参数，它们把输入映射到不同的&quot;空间&quot;，方便做匹配。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;mermaid&quot; class=&quot;language-mermaid &quot;&gt;&lt;code class=&quot;language-mermaid&quot; data-lang=&quot;mermaid&quot;&gt;graph LR
    X[&amp;quot;输入序列 X&amp;quot;] --&amp;gt; WQ[&amp;quot;W_Q&amp;quot;] --&amp;gt; Q
    X --&amp;gt; WK[&amp;quot;W_K&amp;quot;] --&amp;gt; K
    X --&amp;gt; WV[&amp;quot;W_V&amp;quot;] --&amp;gt; V
    Q --&amp;gt; Attn[&amp;quot;Attention&amp;lt;br&amp;#x2F;&amp;gt;Q·K^T&amp;quot;]
    K --&amp;gt; Attn
    Attn --&amp;gt; Softmax[&amp;quot;softmax&amp;lt;br&amp;#x2F;&amp;gt;归一化&amp;quot;]
    V --&amp;gt; Weighted[&amp;quot;加权求和&amp;quot;]
    Softmax --&amp;gt; Weighted
    Weighted --&amp;gt; Out[&amp;quot;输出 Z&amp;quot;]
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;suo-fang-yin-zi-sqrtd-k-shi-gan-ma-de&quot;&gt;缩放因子 √d_k 是干嘛的&lt;a class=&quot;zola-anchor&quot; href=&quot;#suo-fang-yin-zi-sqrtd-k-shi-gan-ma-de&quot; aria-label=&quot;Anchor link for: suo-fang-yin-zi-sqrtd-k-shi-gan-ma-de&quot;&gt;&lt;i class=&quot;fas fa-link&quot;&gt;&lt;&#x2F;i&gt;&lt;&#x2F;a&gt; 
&lt;&#x2F;h3&gt;
&lt;p&gt;这是我当时看了半天才理解的点。&lt;&#x2F;p&gt;
&lt;p&gt;Q 和 K 做点积的时候，如果向量维度 d_k 很大，点积结果也会很大。比如 d_k=64，点积的方差大概是 64，那算出来的分数可能分布在几十到几百的范围。然后 softmax 对这么大的数值做归一化，结果就接近 one-hot 了——几乎所有的权重集中在一个位置上，其他位置几乎为零。&lt;&#x2F;p&gt;
&lt;p&gt;这会有什么后果？梯度几乎为零，模型学不动了，俗称注意力坍缩。&lt;&#x2F;p&gt;
&lt;p&gt;除以 √d_k 本质上就是控制方差，把数值范围拉回到合适的区间，让 softmax 能输出比较平滑的分布。从数学角度说就是做了一次方差归一化。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;er-zi-zhu-yi-li-ji-zhi&quot;&gt;二、自注意力机制&lt;a class=&quot;zola-anchor&quot; href=&quot;#er-zi-zhu-yi-li-ji-zhi&quot; aria-label=&quot;Anchor link for: er-zi-zhu-yi-li-ji-zhi&quot;&gt;&lt;i class=&quot;fas fa-link&quot;&gt;&lt;&#x2F;i&gt;&lt;&#x2F;a&gt; 
&lt;&#x2F;h2&gt;
&lt;p&gt;到这里我们讲了通用的注意力机制。&lt;strong&gt;自注意力&lt;&#x2F;strong&gt;（Self-Attention）的特殊之处在于：Q、K、V 全部来自&lt;strong&gt;同一个输入序列&lt;&#x2F;strong&gt;。也就是说，序列里的每个元素&quot;自己和自己玩&quot;，互相看对方。&lt;&#x2F;p&gt;
&lt;p&gt;拿句子&quot;我在看书&quot;来说：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;&quot;我&quot;作为 Q，去匹配&quot;在&quot;、&quot;看&quot;、&quot;书&quot;的 K&lt;&#x2F;li&gt;
&lt;li&gt;算出&quot;我&quot;和每个词的关联度&lt;&#x2F;li&gt;
&lt;li&gt;按这个关联度重新加权&quot;我&quot;的表示&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;结果就是&quot;我&quot;这个向量不再只是&quot;我&quot;这个词本身的意思，而是融合了它在这个句子中的上下文信息。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;mermaid&quot; class=&quot;language-mermaid &quot;&gt;&lt;code class=&quot;language-mermaid&quot; data-lang=&quot;mermaid&quot;&gt;graph TD
    subgraph &amp;quot;输入序列&amp;quot;
        T1[&amp;quot;我&amp;quot;]
        T2[&amp;quot;在&amp;quot;]
        T3[&amp;quot;看&amp;quot;]
        T4[&amp;quot;书&amp;quot;]
    end

    subgraph &amp;quot;自注意力层&amp;quot;
        direction LR
        Q1[&amp;quot;Q(我)&amp;quot;] --&amp;gt; Dot[&amp;quot;点积匹配&amp;quot;]
        K2[&amp;quot;K(在)&amp;quot;] --&amp;gt; Dot
        K3[&amp;quot;K(看)&amp;quot;] --&amp;gt; Dot
        K4[&amp;quot;K(书)&amp;quot;] --&amp;gt; Dot
        Dot --&amp;gt; W[&amp;quot;softmax → 权重&amp;quot;]
        W --&amp;gt; Sum[&amp;quot;加权求和&amp;quot;]
        V1[&amp;quot;V(我)&amp;quot;] --&amp;gt; Sum
        V2[&amp;quot;V(在)&amp;quot;] --&amp;gt; Sum
        V3[&amp;quot;V(看)&amp;quot;] --&amp;gt; Sum
        V4[&amp;quot;V(书)&amp;quot;] --&amp;gt; Sum
    end

    Sum --&amp;gt; Out[&amp;quot;输出：上下文感知的向量&amp;quot;]
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;下面是假代码，帮理解实际计算长什么样：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;python&quot; class=&quot;language-python &quot;&gt;&lt;code class=&quot;language-python&quot; data-lang=&quot;python&quot;&gt;import numpy as np

def self_attention(X, W_q, W_k, W_v):
    # X: (seq_len, d_model) - 输入序列，每个词是一个 d_model 维的向量
    Q = X @ W_q  # (seq_len, d_k) - 每个词变成&amp;quot;我想找什么&amp;quot;
    K = X @ W_k  # (seq_len, d_k) - 每个词变成&amp;quot;我有什么标签&amp;quot;
    V = X @ W_v  # (seq_len, d_v) - 每个词变成&amp;quot;我的实际内容&amp;quot;

    # 每个词跟所有词算相似度，除以 sqrt(d_k) 防止数值爆炸
    scores = Q @ K.T &amp;#x2F; np.sqrt(d_k)  # (seq_len, seq_len)
    
    # softmax 把分数变成概率分布，和为 1
    weights = softmax(scores, axis=-1)
    
    # 按权重对所有词的内容加权求和，得到融合了上下文的新表示
    output = weights @ V  # (seq_len, d_v)
    return output
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;当时看到这里我悟了一件事：自注意力其实就是&lt;strong&gt;动态加权平均&lt;&#x2F;strong&gt;。每个词的输出表示都是整个序列的加权和，而权重不是固定的，是动态算出来的。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;san-duo-tou-zhu-yi-li&quot;&gt;三、多头注意力&lt;a class=&quot;zola-anchor&quot; href=&quot;#san-duo-tou-zhu-yi-li&quot; aria-label=&quot;Anchor link for: san-duo-tou-zhu-yi-li&quot;&gt;&lt;i class=&quot;fas fa-link&quot;&gt;&lt;&#x2F;i&gt;&lt;&#x2F;a&gt; 
&lt;&#x2F;h2&gt;
&lt;p&gt;一个自注意力头能学到一种关注模式，但句子中词之间的关系是多维的。比如：&lt;&#x2F;p&gt;
&lt;p&gt;&quot;苹果很好吃&quot;和&quot;苹果发布了新手机&quot;&lt;&#x2F;p&gt;
&lt;p&gt;两个&quot;苹果&quot;含义完全不同，前者需要关注&quot;好吃&quot;，后者需要关注&quot;发布了&quot;。一个注意力头要同时捕捉这么多不同的关系，有点难为它了。&lt;&#x2F;p&gt;
&lt;p&gt;多头注意力（Multi-Head Attention）的思路很直接：&lt;strong&gt;用多组 QKV，每组学一种关系&lt;&#x2F;strong&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;具体来说：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;把输入分别投影到 h 组不同的 Q、K、V 空间&lt;&#x2F;li&gt;
&lt;li&gt;每组独立算一次注意力，得到 h 个不同的结果&lt;&#x2F;li&gt;
&lt;li&gt;把这 h 个结果拼起来，再投影回原始维度&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;pre data-lang=&quot;mermaid&quot; class=&quot;language-mermaid &quot;&gt;&lt;code class=&quot;language-mermaid&quot; data-lang=&quot;mermaid&quot;&gt;graph TD
    X[&amp;quot;输入&amp;quot;] --&amp;gt; Linear[&amp;quot;线性投影 × h 组&amp;quot;]
    Linear --&amp;gt; H1[&amp;quot;Head 1&amp;lt;br&amp;#x2F;&amp;gt;Q₁K₁V₁&amp;quot;]
    Linear --&amp;gt; H2[&amp;quot;Head 2&amp;lt;br&amp;#x2F;&amp;gt;Q₂K₂V₂&amp;quot;]
    Linear --&amp;gt; Hn[&amp;quot;... Head h&amp;lt;br&amp;#x2F;&amp;gt;QₕKₕVₕ&amp;quot;]
    H1 --&amp;gt; A1[&amp;quot;Attention₁&amp;quot;]
    H2 --&amp;gt; A2[&amp;quot;Attention₂&amp;quot;]
    Hn --&amp;gt; An[&amp;quot;Attentionₕ&amp;quot;]
    A1 --&amp;gt; Concat[&amp;quot;拼接 Concat&amp;quot;]
    A2 --&amp;gt; Concat
    An --&amp;gt; Concat
    Concat --&amp;gt; WO[&amp;quot;输出投影 W_O&amp;quot;]
    WO --&amp;gt; Out[&amp;quot;输出&amp;quot;]
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;每个头可以关注不同的东西。&lt;a href=&quot;https:&#x2F;&#x2F;towardsdatascience.com&#x2F;visualizing-attention-in-transformer-models-f8f8e8f8e8f8&quot;&gt;一些可视化研究&lt;&#x2F;a&gt;发现：有的头关注语法关系（冠词+名词），有的头关注长距离依赖（句首主语 + 句末动词），还有的头专注位置关系。&lt;&#x2F;p&gt;
&lt;p&gt;这就好比让你同时问好几个专家同一个问题——有人关注细节，有人看整体，最后把大家的意见汇总，理解更全面。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;can-shu-liang-jian-xi&quot;&gt;参数量简析&lt;a class=&quot;zola-anchor&quot; href=&quot;#can-shu-liang-jian-xi&quot; aria-label=&quot;Anchor link for: can-shu-liang-jian-xi&quot;&gt;&lt;i class=&quot;fas fa-link&quot;&gt;&lt;&#x2F;i&gt;&lt;&#x2F;a&gt; 
&lt;&#x2F;h3&gt;
&lt;p&gt;多头注意力的参数量其实没有想象中那么多。因为每组 QKV 的投影是把 d_model 拆成 h 份，每份维度 d_k = d_model &#x2F; h。总参数量和一整组 QKV（不分头）是一样的，只是做了拆分。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;python&quot; class=&quot;language-python &quot;&gt;&lt;code class=&quot;language-python&quot; data-lang=&quot;python&quot;&gt;class MultiHeadAttention:
    def __init__(self, d_model, num_heads):
        self.d_model = d_model
        self.num_heads = num_heads
        self.d_k = d_model &amp;#x2F;&amp;#x2F; num_heads  # 每个头的维度 = 总维度 &amp;#x2F; 头数

        # 四个线性层，每个都是 d_model -&amp;gt; d_model
        # 注意：不是每个头一个线性层，而是整体投影后再拆分
        self.W_q = Linear(d_model, d_model)
        self.W_k = Linear(d_model, d_model)
        self.W_v = Linear(d_model, d_model)
        self.W_o = Linear(d_model, d_model)

    def forward(self, X):
        batch_size, seq_len, _ = X.shape

        # 投影后拆成多个头，shape 变成 (batch, seq, num_heads, d_k)
        Q = self.W_q(X).reshape(batch_size, seq_len, self.num_heads, self.d_k)
        K = self.W_k(X).reshape(batch_size, seq_len, self.num_heads, self.d_k)
        V = self.W_v(X).reshape(batch_size, seq_len, self.num_heads, self.d_k)

        # 转置后所有头可以并行算，不用循环一个个来
        # 这就是 Transformer 能充分利用 GPU 的关键
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;所有头是&lt;strong&gt;并行计算&lt;&#x2F;strong&gt;的，不是循环一个一个算。这也是 Transformer 能充分利用 GPU 的原因。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;si-liang-chong-zhong-yao-de-zhu-yi-li-bian-ti&quot;&gt;四、两种重要的注意力变体&lt;a class=&quot;zola-anchor&quot; href=&quot;#si-liang-chong-zhong-yao-de-zhu-yi-li-bian-ti&quot; aria-label=&quot;Anchor link for: si-liang-chong-zhong-yao-de-zhu-yi-li-bian-ti&quot;&gt;&lt;i class=&quot;fas fa-link&quot;&gt;&lt;&#x2F;i&gt;&lt;&#x2F;a&gt; 
&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;yan-ma-zi-zhu-yi-li-yin-guo-zhu-yi-li&quot;&gt;掩码自注意力（因果注意力）&lt;a class=&quot;zola-anchor&quot; href=&quot;#yan-ma-zi-zhu-yi-li-yin-guo-zhu-yi-li&quot; aria-label=&quot;Anchor link for: yan-ma-zi-zhu-yi-li-yin-guo-zhu-yi-li&quot;&gt;&lt;i class=&quot;fas fa-link&quot;&gt;&lt;&#x2F;i&gt;&lt;&#x2F;a&gt; 
&lt;&#x2F;h3&gt;
&lt;p&gt;做文本生成的时候（比如 GPT 系列），模型要&lt;strong&gt;一个一个词地预测&lt;&#x2F;strong&gt;。预测第 t 个词时，只能看前 t-1 个词，不能偷看后面的内容。&lt;&#x2F;p&gt;
&lt;p&gt;实现方式是在注意力分数上加上一个&lt;strong&gt;上三角掩码&lt;&#x2F;strong&gt;（mask），把未来位置的分数设成负无穷。softmax 之后这些位置的权重就会变成零。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;mermaid&quot; class=&quot;language-mermaid &quot;&gt;&lt;code class=&quot;language-mermaid&quot; data-lang=&quot;mermaid&quot;&gt;graph TD
    subgraph &amp;quot;注意力分数矩阵&amp;quot;
        M[&amp;quot;我 在 看 书&amp;lt;br&amp;#x2F;&amp;gt;我[1  0  0  0]&amp;lt;br&amp;#x2F;&amp;gt;在[1  1  0  0]&amp;lt;br&amp;#x2F;&amp;gt;看[1  1  1  0]&amp;lt;br&amp;#x2F;&amp;gt;书[1  1  1  1]&amp;quot;]
    end
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;1 表示能看到，0 表示被遮住。这就是为什么它叫&quot;因果注意力&quot;——只能基于过去预测未来，符合因果关系。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;jiao-cha-zhu-yi-li&quot;&gt;交叉注意力&lt;a class=&quot;zola-anchor&quot; href=&quot;#jiao-cha-zhu-yi-li&quot; aria-label=&quot;Anchor link for: jiao-cha-zhu-yi-li&quot;&gt;&lt;i class=&quot;fas fa-link&quot;&gt;&lt;&#x2F;i&gt;&lt;&#x2F;a&gt; 
&lt;&#x2F;h3&gt;
&lt;p&gt;在 Encoder-Decoder 架构中（比如翻译任务），除了每个模块自己的自注意力，还需要在 Decoder 里加一个&lt;strong&gt;跨模块的注意力&lt;&#x2F;strong&gt;，让 Decoder 能看到 Encoder 的输出。&lt;&#x2F;p&gt;
&lt;p&gt;它的 Q 来自 Decoder，K 和 V 来自 Encoder。这样 Decoder 在生成每个词的时候，可以去&quot;查&quot;源句子的哪些部分跟自己当前要生成的词最相关。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;mermaid&quot; class=&quot;language-mermaid &quot;&gt;&lt;code class=&quot;language-mermaid&quot; data-lang=&quot;mermaid&quot;&gt;graph LR
    subgraph &amp;quot;编码器 Encoder&amp;quot;
        ENC[&amp;quot;输出表示&amp;quot;]
    end
    subgraph &amp;quot;解码器 Decoder&amp;quot;
        Q_dec[&amp;quot;Q（来自解码器）&amp;quot;]
        AttnCross[&amp;quot;交叉注意力&amp;quot;]
        K_enc[&amp;quot;K（来自编码器）&amp;quot;]
        V_enc[&amp;quot;V（来自编码器）&amp;quot;]
    end
    ENC --&amp;gt; K_enc
    ENC --&amp;gt; V_enc
    Q_dec --&amp;gt; AttnCross
    K_enc --&amp;gt; AttnCross
    V_enc --&amp;gt; AttnCross
    AttnCross --&amp;gt; Out[&amp;quot;上下文向量 → 生成&amp;quot;]
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;wu-zheng-ti-jia-gou&quot;&gt;五、整体架构&lt;a class=&quot;zola-anchor&quot; href=&quot;#wu-zheng-ti-jia-gou&quot; aria-label=&quot;Anchor link for: wu-zheng-ti-jia-gou&quot;&gt;&lt;i class=&quot;fas fa-link&quot;&gt;&lt;&#x2F;i&gt;&lt;&#x2F;a&gt; 
&lt;&#x2F;h2&gt;
&lt;p&gt;把上面所有的模块拼在一起，就是完整的 Transformer。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;mermaid&quot; class=&quot;language-mermaid &quot;&gt;&lt;code class=&quot;language-mermaid&quot; data-lang=&quot;mermaid&quot;&gt;graph TD
    subgraph &amp;quot;编码器 Encoder × N&amp;quot;
        In[&amp;quot;输入序列&amp;quot;] --&amp;gt; PE1[&amp;quot;位置编码&amp;quot;]
        PE1 --&amp;gt; SA1[&amp;quot;自注意力&amp;quot;]
        SA1 --&amp;gt; FFN1[&amp;quot;前馈网络&amp;quot;]
        FFN1 --&amp;gt; OutEnc[&amp;quot;编码器输出&amp;quot;]
    end
    subgraph &amp;quot;解码器 Decoder × N&amp;quot;
        Target[&amp;quot;目标序列&amp;quot;] --&amp;gt; PE2[&amp;quot;位置编码&amp;quot;]
        PE2 --&amp;gt; MSA[&amp;quot;掩码自注意力&amp;quot;]
        MSA --&amp;gt; CA[&amp;quot;交叉注意力&amp;quot;]
        OutEnc --&amp;gt; CA
        CA --&amp;gt; FFN2[&amp;quot;前馈网络&amp;quot;]
        FFN2 --&amp;gt; Linear[&amp;quot;线性层&amp;quot;]
        Linear --&amp;gt; Softmax[&amp;quot;Softmax&amp;quot;]
        Softmax --&amp;gt; Pred[&amp;quot;预测下一个词&amp;quot;]
    end
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;编码器把输入序列转成一组语义表示，解码器基于这组表示逐步生成输出。&lt;&#x2F;p&gt;
&lt;p&gt;但需要注意，现在的主流大语言模型（GPT、Llama、ChatGLM 等）基本都是 &lt;strong&gt;Decoder-only&lt;&#x2F;strong&gt; 架构，不再使用 Encoder。因为 Decoder-only 在文本生成上更高效，而且随着模型变大，纯 Decoder 也能学到很好的表示。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;liu-wei-shen-me-transformer-neng-scale&quot;&gt;六、为什么 Transformer 能 Scale&lt;a class=&quot;zola-anchor&quot; href=&quot;#liu-wei-shen-me-transformer-neng-scale&quot; aria-label=&quot;Anchor link for: liu-wei-shen-me-transformer-neng-scale&quot;&gt;&lt;i class=&quot;fas fa-link&quot;&gt;&lt;&#x2F;i&gt;&lt;&#x2F;a&gt; 
&lt;&#x2F;h2&gt;
&lt;p&gt;这是我觉得最值得思考的一个问题。&lt;&#x2F;p&gt;
&lt;p&gt;在 Transformer 之前，最好的序列模型是 LSTM。但 LSTM 受限于循环结构，参数规模做到亿级就已经很吃力了。而 Transformer 能一路做到千亿、万亿参数，原因主要有这几个：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;并行计算&lt;&#x2F;strong&gt;：自注意力是一次性处理整个序列，完全不依赖前一步的结果。这让 GPU 的矩阵运算能力可以充分发挥&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;信息直达&lt;&#x2F;strong&gt;：任意两个位置的词之间只隔了一层注意力计算，信息传递路径短，长距离依赖不再是问题&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;结构简单&lt;&#x2F;strong&gt;：没有复杂的门控单元（LSTM 的 forget gate、input gate 那一套），就是注意力加前馈网络反复堆叠，简洁的结构反而更利于规模化&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;随着参数增长，模型还涌现出很多小模型没有的能力——上下文学习、思维链推理等。这些现象到现在也没有完全被解释清楚，我觉得这也是这个领域最吸引人的地方。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;qi-huan-mei-wan-quan-gao-dong-de-bu-fen&quot;&gt;七、还没完全搞懂的部分&lt;a class=&quot;zola-anchor&quot; href=&quot;#qi-huan-mei-wan-quan-gao-dong-de-bu-fen&quot; aria-label=&quot;Anchor link for: qi-huan-mei-wan-quan-gao-dong-de-bu-fen&quot;&gt;&lt;i class=&quot;fas fa-link&quot;&gt;&lt;&#x2F;i&gt;&lt;&#x2F;a&gt; 
&lt;&#x2F;h2&gt;
&lt;p&gt;写这篇文章的时候，我还有很多地方处于&quot;好像懂了&quot;到&quot;真懂了&quot;之间的状态：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;层归一化的具体位置&lt;&#x2F;strong&gt;：Pre-Norm 和 Post-Norm 对训练的影响，实验上确实有差异，但我对背后的理论分析还没吃透&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;KV-Cache 的实现细节&lt;&#x2F;strong&gt;：概念上知道是缓存之前的 K、V 避免重复计算，但实际工程里的内存管理、量化策略又是另一回事&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;GQA &#x2F; MLA&lt;&#x2F;strong&gt;：在 MHA 基础上的各种优化变体，原理大概能讲，但为什么某些方案效果更好需要看更多实验分析&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;这些东西等着慢慢啃吧。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;liu-gei-zi-ji-de-yi-ge-si-kao-ti&quot;&gt;留给自己的一个思考题&lt;a class=&quot;zola-anchor&quot; href=&quot;#liu-gei-zi-ji-de-yi-ge-si-kao-ti&quot; aria-label=&quot;Anchor link for: liu-gei-zi-ji-de-yi-ge-si-kao-ti&quot;&gt;&lt;i class=&quot;fas fa-link&quot;&gt;&lt;&#x2F;i&gt;&lt;&#x2F;a&gt; 
&lt;&#x2F;h2&gt;
&lt;p&gt;回过头来看，理解 Transformer 在我心里就是几个核心认知的转变：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;从顺序到并行&lt;&#x2F;strong&gt;：RNN 的词序处理 → 一次性看全部&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;从固定权重到动态权重&lt;&#x2F;strong&gt;：注意力机制让模型自己决定关注什么&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;从单一视角到多个视角&lt;&#x2F;strong&gt;：多头注意力扩展了模型的理解维度&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;从结构限制到结构极简&lt;&#x2F;strong&gt;：简单的组件反复堆叠，反而能涌现复杂能力&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;不过有个问题我还没想明白：为什么简单的&quot;注意力 + 前馈&quot;堆叠起来，就能产生&quot;理解&quot;和&quot;推理&quot;的能力？这到底是量变引起质变，还是我们还没发现的某种机制在起作用？&lt;&#x2F;p&gt;
</content>
	</entry>
	<entry xml:lang="en">
		<title>我理解的 Solidity 存储模型：Storage、Memory 与 Calldata</title>
		<published>2023-08-15T00:00:00+00:00</published>
		<updated>2023-08-15T00:00:00+00:00</updated>
		<link href="https://trinkle23897.pages.dev/solidity-storage-model/"/>
		<link rel="alternate" href="https://trinkle23897.pages.dev/solidity-storage-model/" type="text/html"/>
		<id>https://trinkle23897.pages.dev/solidity-storage-model/</id>
		<content type="html">&lt;h2 id=&quot;xie-zai-qian-mian&quot;&gt;写在前面&lt;a class=&quot;zola-anchor&quot; href=&quot;#xie-zai-qian-mian&quot; aria-label=&quot;Anchor link for: xie-zai-qian-mian&quot;&gt;&lt;i class=&quot;fas fa-link&quot;&gt;&lt;&#x2F;i&gt;&lt;&#x2F;a&gt; 
&lt;&#x2F;h2&gt;
&lt;p&gt;刚开始学 Solidity 的时候，我觉得 Storage、Memory、Calldata 不就是&quot;变量放哪儿&quot;的区别嘛，能有多复杂？直到我写了一个 Token 合约，改了个用户的余额，链上查的时候发现余额纹丝不动——代码没报错，测试也过了，但数据就是没写进去。&lt;&#x2F;p&gt;
&lt;p&gt;后来排查了半天才发现：我把 &lt;code&gt;storage&lt;&#x2F;code&gt; 写成了 &lt;code&gt;memory&lt;&#x2F;code&gt;，改的是副本，链上的数据根本没动。&lt;&#x2F;p&gt;
&lt;p&gt;这件事之后我才认真去搞 EVM 的存储模型到底是怎么回事。这篇文章就是那时候的笔记，把踩过的坑和学到的东西都记下来。&lt;&#x2F;p&gt;
&lt;hr &#x2F;&gt;
&lt;h2 id=&quot;yi-xian-gao-qing-chu-san-zhe-de-ben-zhi&quot;&gt;一、先搞清楚三者的本质&lt;a class=&quot;zola-anchor&quot; href=&quot;#yi-xian-gao-qing-chu-san-zhe-de-ben-zhi&quot; aria-label=&quot;Anchor link for: yi-xian-gao-qing-chu-san-zhe-de-ben-zhi&quot;&gt;&lt;i class=&quot;fas fa-link&quot;&gt;&lt;&#x2F;i&gt;&lt;&#x2F;a&gt; 
&lt;&#x2F;h2&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;维度&lt;&#x2F;th&gt;&lt;th&gt;Storage&lt;&#x2F;th&gt;&lt;th&gt;Memory&lt;&#x2F;th&gt;&lt;th&gt;Calldata&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;生命周期&lt;&#x2F;td&gt;&lt;td&gt;永久写入区块链&lt;&#x2F;td&gt;&lt;td&gt;函数执行期间临时存在&lt;&#x2F;td&gt;&lt;td&gt;交易数据，只读不可修改&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Gas 成本（写）&lt;&#x2F;td&gt;&lt;td&gt;20,000&lt;&#x2F;td&gt;&lt;td&gt;3&#x2F;字&lt;&#x2F;td&gt;&lt;td&gt;不可写&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Gas 成本（读）&lt;&#x2F;td&gt;&lt;td&gt;2,100&lt;&#x2F;td&gt;&lt;td&gt;几乎免费&lt;&#x2F;td&gt;&lt;td&gt;几乎免费&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;典型用途&lt;&#x2F;td&gt;&lt;td&gt;状态变量、用户余额&lt;&#x2F;td&gt;&lt;td&gt;临时变量、函数内部计算&lt;&#x2F;td&gt;&lt;td&gt;外部函数的数组&#x2F;字符串参数&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;一句话总结：Storage 是硬盘，Memory 是内存，Calldata 是只读缓存。记住这个，后面选起来就清楚了。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;er-storage-xie-jin-qu-jiu-shi-qian&quot;&gt;二、Storage：写进去就是钱&lt;a class=&quot;zola-anchor&quot; href=&quot;#er-storage-xie-jin-qu-jiu-shi-qian&quot; aria-label=&quot;Anchor link for: er-storage-xie-jin-qu-jiu-shi-qian&quot;&gt;&lt;i class=&quot;fas fa-link&quot;&gt;&lt;&#x2F;i&gt;&lt;&#x2F;a&gt; 
&lt;&#x2F;h2&gt;
&lt;p&gt;任何需要跨交易保留的数据，只能放 Storage。没有替代方案。&lt;&#x2F;p&gt;
&lt;p&gt;状态变量天然在 Storage 里：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;solidity&quot; class=&quot;language-solidity &quot;&gt;&lt;code class=&quot;language-solidity&quot; data-lang=&quot;solidity&quot;&gt;contract Bank {
    &amp;#x2F;&amp;#x2F; 这些状态变量自动存在 Storage 里，交易结束数据还在
    mapping(address =&amp;gt; uint256) public balances;
    address public owner;
    uint256 public totalDeposits;

    function deposit() external payable {
        balances[msg.sender] += msg.value;  &amp;#x2F;&amp;#x2F; 写 Storage，20000 Gas，肉疼但必须花
        totalDeposits += msg.value;
    }
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;用户存了钱，下次来必须还能查到余额——这就是 Storage 的价值。&lt;&#x2F;p&gt;
&lt;p&gt;但反过来，&lt;strong&gt;不要把临时计算结果往 Storage 里塞&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;solidity&quot; class=&quot;language-solidity &quot;&gt;&lt;code class=&quot;language-solidity&quot; data-lang=&quot;solidity&quot;&gt;contract BadExample {
    uint256 public tempResult;  &amp;#x2F;&amp;#x2F; ❌ 这玩意儿没必要永久存链上

    function calculate(uint256 a, uint256 b) external {
        tempResult = a + b;     &amp;#x2F;&amp;#x2F; ❌ 每次调用花 20000 Gas 存一个临时结果
    }
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;code&gt;tempResult&lt;&#x2F;code&gt; 就是个中间结果，没有跨交易保留的必要。改成 pure 函数，一分钱 Storage 费用都不用花：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;solidity&quot; class=&quot;language-solidity &quot;&gt;&lt;code class=&quot;language-solidity&quot; data-lang=&quot;solidity&quot;&gt;contract GoodExample {
    &amp;#x2F;&amp;#x2F; ✅ 纯计算，不碰 Storage，Gas 几乎为零
    function calculate(uint256 a, uint256 b) external pure returns (uint256) {
        return a + b;
    }
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;san-memory-han-shu-jie-shu-jiu-xiao-shi&quot;&gt;三、Memory：函数结束就消失&lt;a class=&quot;zola-anchor&quot; href=&quot;#san-memory-han-shu-jie-shu-jiu-xiao-shi&quot; aria-label=&quot;Anchor link for: san-memory-han-shu-jie-shu-jiu-xiao-shi&quot;&gt;&lt;i class=&quot;fas fa-link&quot;&gt;&lt;&#x2F;i&gt;&lt;&#x2F;a&gt; 
&lt;&#x2F;h2&gt;
&lt;p&gt;Memory 用于函数执行期间的临时数据。函数返回，数据消失，链上不留痕迹。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;wo-cai-de-na-ge-keng&quot;&gt;我踩的那个坑&lt;a class=&quot;zola-anchor&quot; href=&quot;#wo-cai-de-na-ge-keng&quot; aria-label=&quot;Anchor link for: wo-cai-de-na-ge-keng&quot;&gt;&lt;i class=&quot;fas fa-link&quot;&gt;&lt;&#x2F;i&gt;&lt;&#x2F;a&gt; 
&lt;&#x2F;h3&gt;
&lt;p&gt;就是开头说的那个事儿，简化一下大概长这样：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;solidity&quot; class=&quot;language-solidity &quot;&gt;&lt;code class=&quot;language-solidity&quot; data-lang=&quot;solidity&quot;&gt;contract BadToken {
    struct User {
        uint256 balance;
        uint256 lastDeposit;
    }
    mapping(address =&amp;gt; User) public users;

    function updateLastDeposit(address user) external {
        &amp;#x2F;&amp;#x2F; 这里用 memory，创建的是 Storage 数据的副本
        User memory u = users[user];
        &amp;#x2F;&amp;#x2F; 改的是副本！链上的 users[user] 纹丝不动
        &amp;#x2F;&amp;#x2F; 代码能跑，测试能过（如果你没重新读 Storage 验证的话）
        &amp;#x2F;&amp;#x2F; 但数据就是没写进去——静默错误，最恶心
        u.lastDeposit = block.timestamp;
    }
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;修复方法：把 &lt;code&gt;memory&lt;&#x2F;code&gt; 换成 &lt;code&gt;storage&lt;&#x2F;code&gt;，变成引用：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;solidity&quot; class=&quot;language-solidity &quot;&gt;&lt;code class=&quot;language-solidity&quot; data-lang=&quot;solidity&quot;&gt;contract GoodToken {
    struct User {
        uint256 balance;
        uint256 lastDeposit;
    }
    mapping(address =&amp;gt; User) public users;

    function updateLastDeposit(address user) external {
        &amp;#x2F;&amp;#x2F; storage 引用，u 就是 users[user] 本身
        User storage u = users[user];
        u.lastDeposit = block.timestamp;  &amp;#x2F;&amp;#x2F; ✅ 直接改链上数据
    }
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;这个区分一定要记住：&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;storage&lt;&#x2F;code&gt; = 引用，修改直接影响链上&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;memory&lt;&#x2F;code&gt; = 副本，修改只在函数内有效&lt;&#x2F;li&gt;
&lt;li&gt;不写关键字时，结构体&#x2F;数组默认看上下文——状态变量是 &lt;code&gt;storage&lt;&#x2F;code&gt;，局部变量是 &lt;code&gt;memory&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;si-calldata-zui-bian-yi-dan-zhi-du&quot;&gt;四、Calldata：最便宜但只读&lt;a class=&quot;zola-anchor&quot; href=&quot;#si-calldata-zui-bian-yi-dan-zhi-du&quot; aria-label=&quot;Anchor link for: si-calldata-zui-bian-yi-dan-zhi-du&quot;&gt;&lt;i class=&quot;fas fa-link&quot;&gt;&lt;&#x2F;i&gt;&lt;&#x2F;a&gt; 
&lt;&#x2F;h2&gt;
&lt;p&gt;Calldata 是交易数据的存储区域，只读、不可修改。它是三种位置中最便宜的。&lt;&#x2F;p&gt;
&lt;p&gt;外部函数的数组参数，&lt;strong&gt;默认就该用 calldata&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;solidity&quot; class=&quot;language-solidity &quot;&gt;&lt;code class=&quot;language-solidity&quot; data-lang=&quot;solidity&quot;&gt;contract BatchTransfer {
    &amp;#x2F;&amp;#x2F; calldata 直接引用交易数据，不复制，省 Gas
    &amp;#x2F;&amp;#x2F; 传 100 个地址进来，省掉 100 次 Memory 分配
    function batchSend(
        address[] calldata recipients,
        uint256[] calldata amounts
    ) external {
        require(recipients.length == amounts.length, &amp;quot;Length mismatch&amp;quot;);
        for (uint256 i = 0; i &amp;lt; recipients.length; i++) {
            &amp;#x2F;&amp;#x2F; 直接读 calldata，不占 Memory
        }
    }
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;如果写成 &lt;code&gt;memory&lt;&#x2F;code&gt;，交易数据会被完整复制一份。外部函数只读不改，这份复制纯粹浪费钱：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;solidity&quot; class=&quot;language-solidity &quot;&gt;&lt;code class=&quot;language-solidity&quot; data-lang=&quot;solidity&quot;&gt;contract BadBatch {
    &amp;#x2F;&amp;#x2F; ❌ memory 会把整个数组复制一份到 Memory
    &amp;#x2F;&amp;#x2F; 100 个地址 = 3200 字节，白白多花的 Gas
    function batchSend(
        address[] memory recipients,
        uint256[] memory amounts
    ) external {
    }
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;当然，如果你需要修改输入数据，那只能复制到 Memory：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;solidity&quot; class=&quot;language-solidity &quot;&gt;&lt;code class=&quot;language-solidity&quot; data-lang=&quot;solidity&quot;&gt;contract SortExample {
    &amp;#x2F;&amp;#x2F; calldata 不可修改，要排序只能复制到 memory
    function sort(uint256[] memory data) external pure returns (uint256[] memory) {
        for (uint256 i = 0; i &amp;lt; data.length; i++) {
            for (uint256 j = i + 1; j &amp;lt; data.length; j++) {
                if (data[i] &amp;gt; data[j]) {
                    (data[i], data[j]) = (data[j], data[i]);
                }
            }
        }
        return data;
    }
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;wu-storage-yin-yong-de-gas-you-hua&quot;&gt;五、Storage 引用的 Gas 优化&lt;a class=&quot;zola-anchor&quot; href=&quot;#wu-storage-yin-yong-de-gas-you-hua&quot; aria-label=&quot;Anchor link for: wu-storage-yin-yong-de-gas-you-hua&quot;&gt;&lt;i class=&quot;fas fa-link&quot;&gt;&lt;&#x2F;i&gt;&lt;&#x2F;a&gt; 
&lt;&#x2F;h2&gt;
&lt;p&gt;搞明白 &lt;code&gt;storage&lt;&#x2F;code&gt; 引用之后，我发现这不只是正确性的问题，还能省不少 Gas。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;yi-ci-ding-wei-vs-duo-ci-ding-wei&quot;&gt;一次定位 vs 多次定位&lt;a class=&quot;zola-anchor&quot; href=&quot;#yi-ci-ding-wei-vs-duo-ci-ding-wei&quot; aria-label=&quot;Anchor link for: yi-ci-ding-wei-vs-duo-ci-ding-wei&quot;&gt;&lt;i class=&quot;fas fa-link&quot;&gt;&lt;&#x2F;i&gt;&lt;&#x2F;a&gt; 
&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;solidity&quot; class=&quot;language-solidity &quot;&gt;&lt;code class=&quot;language-solidity&quot; data-lang=&quot;solidity&quot;&gt;contract MultiUser {
    struct User {
        uint256 balance;
        bool isActive;
    }
    mapping(address =&amp;gt; User) public users;

    function updateUser(address user, uint256 newBalance, bool active) external {
        &amp;#x2F;&amp;#x2F; 一次 storage 定位（2100 Gas），后续字段修改都在这一次定位内完成
        User storage u = users[user];
        u.balance = newBalance;   &amp;#x2F;&amp;#x2F; ✅ 不用再花 Gas 定位
        u.isActive = active;      &amp;#x2F;&amp;#x2F; ✅ 同上
    }
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;如果不引用，每次都重新定位：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;solidity&quot; class=&quot;language-solidity &quot;&gt;&lt;code class=&quot;language-solidity&quot; data-lang=&quot;solidity&quot;&gt;contract Inefficient {
    mapping(address =&amp;gt; uint256) public balances;

    function update(address user) external {
        balances[user] += 10;   &amp;#x2F;&amp;#x2F; ❌ 第一次定位
        balances[user] += 20;   &amp;#x2F;&amp;#x2F; ❌ 第二次定位
        balances[user] += 30;   &amp;#x2F;&amp;#x2F; ❌ 第三次定位
        &amp;#x2F;&amp;#x2F; 三次独立的 Storage 读写，每次都花 Gas
    }
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;更好的做法是缓存到 Memory 再写回：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;solidity&quot; class=&quot;language-solidity &quot;&gt;&lt;code class=&quot;language-solidity&quot; data-lang=&quot;solidity&quot;&gt;contract Efficient {
    mapping(address =&amp;gt; uint256) public balances;

    function update(address user) external {
        uint256 temp = balances[user];  &amp;#x2F;&amp;#x2F; ✅ 只读一次 Storage
        temp += 10;
        temp += 20;
        temp += 30;
        balances[user] = temp;          &amp;#x2F;&amp;#x2F; ✅ 只写一次 Storage
    }
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;liu-jue-ce-liu-cheng&quot;&gt;六、决策流程&lt;a class=&quot;zola-anchor&quot; href=&quot;#liu-jue-ce-liu-cheng&quot; aria-label=&quot;Anchor link for: liu-jue-ce-liu-cheng&quot;&gt;&lt;i class=&quot;fas fa-link&quot;&gt;&lt;&#x2F;i&gt;&lt;&#x2F;a&gt; 
&lt;&#x2F;h2&gt;
&lt;p&gt;遇到一个新变量，按这个顺序判断就行：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;需要跨交易保留？&lt;&#x2F;strong&gt; → Storage&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;只是函数内部临时用？&lt;&#x2F;strong&gt; → Memory&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;外部传入的只读数组&#x2F;字符串？&lt;&#x2F;strong&gt; → Calldata&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;需要修改外部传入的数据？&lt;&#x2F;strong&gt; → Memory（复制后修改）&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;hr &#x2F;&gt;
&lt;p&gt;&lt;em&gt;本文代码基于 Solidity 0.8.x 版本。&lt;&#x2F;em&gt;&lt;&#x2F;p&gt;
</content>
	</entry>
</feed>
