自訂運算子

由於 TensorFlow Lite 內建運算子程式庫僅支援有限的 TensorFlow 運算子,因此並非所有模型都能轉換。詳情請參閱運算子相容性

如要允許轉換,使用者可以在 TensorFlow Lite 中,自行指定不支援的 TensorFlow 運算子 (稱為自訂運算子)。如果您希望將一系列不支援的 (或支援) 的 TensorFlow 運算子合併為單一整合式最佳化自訂運算子,請參閱運算子融合

使用自訂運算子包含四個步驟。

以下將逐步說明用自訂運算子 tf.atan (名為 Atan) 執行模型的端對端範例,請參閱「建立 TensorFlow 模型」一節,但 TensorFlow Lite 不支援此模型。

TensorFlow Text 運算子是自訂運算子的範例。如需程式碼範例,請參閱將 TF 文字轉換為 TF Lite 教學課程。

範例:自訂 Atan 運算子

以下將逐步介紹 TensorFlow Lite 所沒有支援 TensorFlow 運算子的範例。假設我們使用 Atan 運算子,並且正在為函式 y = atan(x + offset) 建構非常簡單的模型,其中 offset 為可訓練。

建立 TensorFlow 模型

下列程式碼片段會訓練簡單的 TensorFlow 模型。這個模型只包含名為 Atan 的自訂運算子,這是 y = atan(x + offset) 函式,其中 offset 可訓練。

import tensorflow as tf

# Define training dataset and variables
x = [-8, 0.5, 2, 2.2, 201]
y = [-1.4288993, 0.98279375, 1.2490457, 1.2679114, 1.5658458]
offset = tf.Variable(0.0)

# Define a simple model which just contains a custom operator named `Atan`
@tf.function(input_signature=[tf.TensorSpec.from_tensor(tf.constant(x))])
def atan(x):
  return tf.atan(x + offset, name="Atan")

# Train model
optimizer = tf.optimizers.Adam(0.01)
def train(x, y):
    with tf.GradientTape() as t:
      predicted_y = atan(x)
      loss = tf.reduce_sum(tf.square(predicted_y - y))
    grads = t.gradient(loss, [offset])
    optimizer.apply_gradients(zip(grads, [offset]))

for i in range(1000):
    train(x, y)

print("The actual offset is: 1.0")
print("The predicted offset is:", offset.numpy())
The actual offset is: 1.0
The predicted offset is: 0.99999905

此時,如果您嘗試使用預設轉換標記產生 TensorFlow Lite 模型,便會收到以下錯誤訊息:

Error:
error: 'tf.Atan' op is neither a custom op nor a flex op.

轉換為 TensorFlow Lite 模型

設定轉換器屬性 allow_custom_ops 以建立包含自訂運算子的 TensorFlow Lite 模型,如下所示:

converter = tf.lite.TFLiteConverter.from_concrete_functions([atan.get_concrete_function()], atan)
converter.allow_custom_ops = True
tflite_model = converter.convert()

此時,如果您是利用預設解譯器執行此指令,請使用以下指令:

interpreter = tf.lite.Interpreter(model_content=tflite_model)
interpreter.allocate_tensors()

您仍然會收到以下錯誤訊息:

Encountered unresolved custom op: Atan.

建立及註冊運算子。

#include "third_party/tensorflow/lite/c/c_api.h"
#include "third_party/tensorflow/lite/c/c_api_opaque.h"

TensorFlow Lite 自訂運算子是以不透明類型 (TfLiteRegistrationExternal) 和相關函式組成的簡易純 C API 定義,

TfLiteRegistrationExternal 是不透明類型:

typedef struct TfLiteRegistrationExternal TfLiteRegistrationExternal;

TfLiteRegistrationExternal 會儲存運算子的身分和實作項目。(請注意,運算子與運算元不同,運算元儲存在 TF Lite 圖形節點中,適用於呼叫運算子的節點)。

此類型的例項是由呼叫 TfLiteRegistrationExternalCreate 所建構,如要刪除,可呼叫 TfLiteRegistrationExternalDelete

透過建構函式函式 TfLiteRegistrationExternalCreate 的參數來設定運算子的身分:

TfLiteRegistrationExternal*
TfLiteRegistrationExternalCreate(
    TfLiteBuiltinOperator builtin_code,  // Normally `TfLiteBuiltinCustom`.
    const char* custom_name,  // The name of the custom op.
    int version  // Normally `1` for the first version of a custom op.
);

運算子實作可使用下列簽名定義「方法」。這些方法皆為選用方法,不過如要成功評估運算子,運算子實作作業至少需要定義和設定 (使用 setter 函式) 至少為 PrepareInvoke 方法。

// Initializes the op from serialized data.
void* Init(TfLiteOpaqueContext* context, const char* buffer, size_t length);

// Deallocates the op.
// The pointer `buffer` is the data previously returned by an Init invocation.
void Free(TfLiteOpaqueContext* context, void* buffer);

// Called when the inputs that this node depends on have been resized.
TfLiteStatus Prepare(TfLiteOpaqueContext* context, TfLiteOpaqueNode* node);

// Called when the node is executed. (Should read node inputs and write to
// node outputs).
TfLiteStatus Invoke(TfLiteOpaqueContext* context, TfLiteOpaqueNode* node);

// Retrieves the async kernel.
TfLiteAsyncKernel AsyncKernel(TfLiteOpaqueContext* context,
                              TfLiteOpaqueNode* node);

在運算實作中,函式「名稱」names (或 C++ 的命名空間前置字串) 不必與上述程式碼片段中的函式名稱相符,因為 TFLite 自訂作業 API 只會使用其地址。確實建議您在匿名命名空間或靜態函式中宣告這些變數。

不過,建議您將運算子名稱新增為這些函式名稱的命名空間或前置字串:

C++

namespace my_namespace::my_custom_op {
  void* Init(TfLiteOpaqueContext* context,
             const char* buffer, size_t length) { ... }
  // ... plus definitions of Free, Prepare, and Invoke ...
}
      

C

void* MyCustomOpInit(TfLiteOpaqueContext* context,
                     const char* buffer, size_t length) { ... }
// ... plus definitions of MyCustomOpFree, MyCustomOpPrepare, and
// MyCustomOpInvoke.
      

由於這是 C API,因此這些「方法」是在 TfLiteRegistrationExternal 類型中以 C 函式指標實作,設定方式是將實作函式的地址傳遞至對應的 setter 函式 TfLiteRegistrationExternalSetMethodName

void TfLiteRegistrationExternalSetInit(
    TfLiteRegistrationExternal* registration,
    void* (*init)(TfLiteOpaqueContext* context, const char* buffer,
                  size_t length));
void TfLiteRegistrationExternalSetFree(
    TfLiteRegistrationExternal* registration,
    void (*free)(TfLiteOpaqueContext* context, void* data));
void TfLiteRegistrationExternalSetPrepare(
    TfLiteRegistrationExternal* registration,
    TfLiteStatus (*prepare)(TfLiteOpaqueContext* context,
                            TfLiteOpaqueNode* node));
void TfLiteRegistrationExternalSetInvoke(
    TfLiteRegistrationExternal* registration,
    TfLiteStatus (*invoke)(TfLiteOpaqueContext* context,
                           TfLiteOpaqueNode* node));
void TfLiteRegistrationExternalSetAsyncKernel(
    TfLiteRegistrationExternal* registration,
    struct TfLiteAsyncKernel* (*async_kernel)(TfLiteOpaqueContext* context,
                                              TfLiteOpaqueNode* node));

如要進一步瞭解 TfLiteContextTfLiteNode,請參閱 common.hTfLiteContext 提供錯誤回報設施和全域物件存取權,包括所有張量。TfLiteNode 可讓運算子實作存取其輸入和輸出內容。

解譯器載入模型時,會對圖表中的每個節點呼叫一次 Init() 方法。如果在圖表中多次使用該運算,系統就會多次呼叫特定 Init()。針對自訂作業,系統會提供設定緩衝區,其中包含可將參數名稱對應至其值的 Flexbuffer。由於解譯器已剖析運算參數,因此內建運算的緩衝區是空白的。需要狀態的核心實作項目應在這裡初始化,並將擁有權轉移給呼叫端。每個 Init() 呼叫都會有對應的 Free() 呼叫,可讓實作作業處理在 Init() 中配置的緩衝區。

每當輸入張量調整大小時,翻譯器都會透過圖表來通知變更實作。如此一來,他們就能夠調整內部緩衝區的大小、檢查輸入形狀和類型是否有效,以及重新計算輸出形狀。這些操作都是透過 Prepare() 方法完成,實作可以使用 TfLiteOpaqueNodeGetUserData(node) 存取其狀態。

最後,每次推論執行時,解譯器都會週遊至呼叫 Invoke() 方法的圖表,此時也能使用 TfLiteOpaqueNodeGetUserData(node)

實作自訂作業時,可以定義這些「方法」函式,然後定義一個函式,該函式可傳回 TfLiteRegistrationExternal 例項,方法是呼叫 TfLiteRegistrationExternalCreate,再呼叫相關的 setter 方法:

C++

namespace my_namespace::my_custom_op {
  namespace {
    void* Init(TfLiteOpaqueContext* context,
               const char* buffer, size_t length) { ... }
    void Free(TfLiteOpaqueContext* context, void* buffer) { ... }
    TfLiteStatus Prepare(TfLiteOpaqueContext* context,
                         TfLiteOpaqueNode* node) { ... }
    TfLiteStatus Invoke(TfLiteOpaqueContext* context,
                        TfLiteOpaqueNode* node) {... }
  };

  const TfLiteRegistrationExternal* MyCustomOpRegistrationExternal() {
    // Singleton instance, intentionally never destroyed.
    static const TfLiteRegistrationExternal* my_custom_op = ()[] {
        TfLiteRegistrationExternal* r =
            TfLiteRegistrationExternalCreate(
                kTfLiteBuiltinCustom, "MyCustomOp", /*version=*/ 1);
        TfLiteRegistrationExternalSetInit(r, Init);
        TfLiteRegistrationExternalSetFree(r, Free);
        TfLiteRegistrationExternalSetPrepare(r, Prepare);
        TfLiteRegistrationExternalSetInvoke(r, Eval);
        return r;
      };
    return my_custom_op;
  }

  const TfLiteRegistration* MyCustomOpRegistration() {
    static const TfLiteRegistration my_custom_op {
      .registration_external = MyCustomOpRegistrationExternal();
    };
    return my_custom_op;
  }
}  // namespace my_namespace
      

C

static void* MyCustomOpInit(TfLiteOpaqueContext* context, const char* buffer,
                     size_t length) { ... }
static void MyCustomOpFree(TfLiteOpaqueContext* context, void* buffer) { ... }
static TfLiteStatus MyCustomOpPrepare(TfLiteOpaqueContext* context,
                                      TfLiteOpaqueNode* node) { ... }
static TfLiteStatus MyCustomOpInvoke(TfLiteOpaqueContext* context,
                                     TfLiteOpaqueNode* node) {... }

static TfLiteRegistrationExternal* MyCustomOpCreate() {
  const TfLiteRegistrationExternal* r =
      TfLiteRegistrationExternalCreate(
          kTfLiteBuiltinCustom, "MyCustomOp", /*version=*/ 1);
  TfLiteRegistrationExternalSetInit(r, MyCustomOpInit);
  TfLiteRegistrationExternalSetFree(r, MyCustomOpFree);
  TfLiteRegistrationExternalSetPrepare(r, MyCustomOpPrepare);
  TfLiteRegistrationExternalSetInvoke(r, MyCustomOpEval);
  return r;
}

const TfLiteRegistrationExternal* MyCustomOpRegistrationExternal() {
  // Singleton instance, intentionally never destroyed.
  static const TfLiteRegistrationExternal* my_custom_op = MyCustomOpCreate();
  return my_custom_op;
}

const TfLiteRegistration MyCustomOpRegistration() {
  static const TfLiteRegistration my_custom_op {
    .registration_external = MyCustomOpRegistrationExternal();
  };
  return my_custom_op;
}
      

請注意,註冊並非自動註冊,應會明確呼叫 MyCustomOpRegistration 函式 (詳情請參閱下方說明)。雖然標準 BuiltinOpResolver (可從 :builtin_ops 目標取得) 負責註冊內建項目,但自訂運算必須透過不同的自訂程式庫進行收集。

在 TensorFlow Lite 執行階段中定義核心

如要在 TensorFlow Lite 中使用運算,我們只需要定義兩個函式 (PrepareEval),以及第三個函式建構 TfLiteRegistrationExternal

C++

namespace atan_op {
  namespace {
    TfLiteStatus AtanPrepare(TfLiteOpaqueContext* context, TfLiteOpaqueNode* node) {
      TF_LITE_OPAQUE_ENSURE_EQ(context, TfLiteOpaqueNodeNumInputs(node), 1);
      TF_LITE_OPAQUE_ENSURE_EQ(context, TfLiteOpaqueNodeNumOutputs(node), 1);

      const TfLiteOpaqueTensor* input = TfLiteOpaqueNodeGetInput(context, node, 0);
      TfLiteOpaqueTensor* output = TfLiteOpaqueNodeGetOutput(context, node, 0);

      int num_dims = TfLiteOpaqueTensorNumDimensions(input);

      TfLiteIntArray* output_size = TfLiteIntArrayCreate(num_dims);
      for (int i=0; i < num_dims; ++i) {
        output_size->data[i] = input->dims->data[i];
      }

      return TfLiteOpaqueContextResizeTensor(context, output, output_size);
    }

    TfLiteStatus AtanEval(TfLiteOpaqueContext* context, TfLiteOpaqueNode* node) {
      const TfLiteOpaqueTensor* input = TfLiteOpaqueNodeGetInput(context, node, 0);
      TfLiteOpaqueTensor* output = TfLiteOpaqueNodeGetOutput(context, node, 0);

      float* input_data = static_cast(TfLiteOpaqueTensorData(input));
      float* output_data = static_cast(TfLiteOpaqueTensorData(output));

      size_t count = 1;
      int num_dims = TfLiteOpaqueTensorNumDimensions(input);
      for (int i = 0; i < num_dims; ++i) {
        count *= input->dims->data[i];
      }

      for (size_t i = 0; i < count; ++i) {
        output_data[i] = atan(input_data[i]);
      }
      return kTfLiteOk;
    }
  }  // anonymous namespace

  const TfLiteRegistrationExternal* AtanOpRegistrationExternal() {
    // Singleton instance, intentionally never destroyed.
    static const TfLiteRegistrationExternal* atan_op = ()[] {
        auto* r = TfLiteRegistrationExternalCreate(
            kTfLiteBuiltinCustom, "ATAN", /*version=*/ 1);
        TfLiteRegistrationExternalSetPrepare(r, Prepare);
        TfLiteRegistrationExternalSetInvoke(r, Eval);
        return r;
      };
    return atan_op;
  }

  const TfLiteRegistration AtanOpRegistration() {
    static const TfLiteRegistration atan_op {
      .registration_external = AtanOpRegistrationExternal();
    };
    return atan_op;
  }
}  // namespace atan_op
      

C

static TfLiteStatus AtanPrepare(TfLiteOpaqueContext* context, TfLiteOpaqueNode* node) {
  TF_LITE_OPAQUE_ENSURE_EQ(context, TfLiteOpaqueNodeNumInputs(node), 1);
  TF_LITE_OPAQUE_ENSURE_EQ(context, TfLiteOpaqueNodeNumOutputs(node), 1);

  const TfLiteOpaqueTensor* input = TfLiteOpaqueNodeGetInput(context, node, 0);
  TfLiteOpaqueTensor* output = TfLiteOpaqueNodeGetOutput(context, node, 0);

  int num_dims = TfLiteOpaqueTensorNumDimensions(input);

  TfLiteIntArray* output_size = TfLiteIntArrayCreate(num_dims);
  for (int i = 0; i < num_dims; ++i) {
    output_size->data[i] = input->dims->data[i];
  }

  return TfLiteOpaqueContextResizeTensor(context, output, output_size);
}

static TfLiteStatus AtanEval(TfLiteOpaqueContext* context, TfLiteOpaqueNode* node) {
  const TfLiteOpaqueTensor* input = TfLiteOpaqueNodeGetInput(context, node, 0);
  TfLiteOpaqueTensor* output = TfLiteOpaqueNodeGetOutput(context, node, 0);

  float* input_data = static_cast(TfLiteOpaqueTensorData(input));
  float* output_data = static_cast(TfLiteOpaqueTensorData(output));

  size_t count = 1;
  int num_dims = TfLiteOpaqueTensorNumDimensions(input);
  for (int i = 0; i < num_dims; ++i) {
    count *= input->dims->data[i];
  }

  for (size_t i = 0; i < count; ++i) {
    output_data[i] = atan(input_data[i]);
  }
  return kTfLiteOk;
}

static const TfLiteRegistrationExternal* AtanOpCreate() {
  TfLiteRegistrationExternal* r = TfLiteRegistrationExternalCreate(
          kTfLiteBuiltinCustom, "ATAN", /*version=*/ 1);
  TfLiteRegistrationExternalSetPrepare(r, Prepare);
  TfLiteRegistrationExternalSetInvoke(r, Eval);
  return r;
}

const TfLiteRegistrationExternal* AtanOpRegistrationExternal() {
  // Singleton instance, intentionally never destroyed.
  static const TfLiteRegistrationExternal* atan_op = AtanOpCreate();
  return atan_op;
}

const TfLiteRegistration AtanOpRegistration() {
  static const TfLiteRegistration atan_op {
    .registration_external = AtanOpRegistrationExternal();
  };
  return atan_op;
}
      

初始化 OpResolver 時,請將自訂運算新增至解析器 (請參閱下方範例)。這會透過 Tensorflow Lite 註冊運算子,讓 TensorFlow Lite 可以使用新的實作。請注意,TfLiteRegistration 中的最後兩個引數會對應到您為自訂運算定義的 AtanPrepareAtanEval 函式。如果您使用 AtanInitAtanFree 函式初始化運算中使用的變數,並分別釋放空間,那麼系統會將這些引數新增至 TfLiteRegistration 的前兩個引數中,在本範例中,這些引數會設為 nullptr

使用核心程式庫註冊運算子

現在,我們需要透過核心程式庫註冊運算子。方法是使用 OpResolver。解譯器會在背景載入核心程式庫,這個程式庫會指派給執行模型中的每個運算子。雖然預設程式庫僅包含內建核心,但您可以使用自訂程式庫運算運算子取代/建構程式庫。

可將運算子代碼和名稱轉譯為實際程式碼的 OpResolver 類別,定義如下:

class OpResolver {
 public:
  virtual TfLiteRegistration* FindOp(tflite::BuiltinOperator op) const = 0;
  virtual TfLiteRegistration* FindOp(const char* op) const = 0;
  ...
};

請注意,為了提供回溯相容性,這個類別使用舊版具體類型 TfLiteRegistration,而非不透明類型 TfLiteRegistrationExternal,但 TfLiteRegistration 結構包含 TfLiteRegistrationExternal* 類型的 registration_external 欄位。

MutableOpResolverBuiltinOpResolver 類別衍生自 OpResolver

class MutableOpResolver : public OpResolver {
 public:
  MutableOpResolver();  // Constructs an initially empty op resolver.
  void AddBuiltin(tflite::BuiltinOperator op, const TfLiteRegistration* registration) = 0;
  void AddCustom(const char* op, const TfLiteRegistration* registration) = 0;
  void AddAll(const MutableOpResolver& other);
  ...
};

class BuiltinOpResolver : public MutableOpResolver {
 public:
  BuiltinOpResolver();  // Constructs an op resolver with all the builtin ops.
};

如要定期使用 (不含自訂作業),您必須使用 BuiltinOpResolver 並寫入:

tflite::ops::builtin::BuiltinOpResolver resolver;

如要新增上述建立的自訂運算,您可以改用 MutableOpResolver,並呼叫 AddCustom (在將解析器傳送至 InterpreterBuilder 之前):

tflite::ops::builtin::MutableOpResolver resolver;
resolver.AddAll(tflite::ops::builtin::BuiltinOpResolver());
resolver.AddCustom("Atan", AtanOpRegistration());

如果內建運算組合過大,系統可以根據指定運算子集來產生新的 OpResolver,可能只有指定模型中包含的項目。這相當於 TensorFlow 的選擇性註冊功能 (也可在 tools 目錄中取得簡易版本)。

如要以 Java 定義自訂運算子,目前您需要在這個 jni 程式碼中自行建構自訂 JNI 層,並自行編譯 AAR。同樣地,如果您想在 Python 中定義這些運算子,可將註冊放入 Python 包裝函式程式碼

請注意,您可以遵循上述類似程序,支援一組作業,而非單一運算子。只要視需要新增多個 AddCustom 運算子即可。此外,MutableOpResolver 還可讓您使用 AddBuiltin 覆寫內建功能的實作。

測試及剖析運算子

如要使用 TensorFlow Lite 基準工具分析運算結果,你可以使用 TensorFlow Lite 的基準測試模型工具。為了進行測試,您可以將適當的 AddCustom 呼叫 (如上所示) 新增至 register.cc,讓本機 TensorFlow Lite 知道您的自訂運算

最佳做法

  1. 謹慎地最佳化記憶體配置和取消配置。在 Prepare 中分配記憶體比在 Invoke 中更有效率,而在迴圈之前分配記憶體會比每次疊代作業更佳。請使用臨時張量資料,而不要自行燒杯 (請參閱項目 2)。盡量使用指標/參照而非複製檔案。

  2. 如果資料結構在整個作業期間都會保持不變,建議您使用臨時張量預先分配記憶體。您可能需要使用 OpData 結構來參照其他函式中的張量索引。請參閱卷積核心中的範例。以下提供程式碼片段範例。

    struct MyOpData {
      int temp_tensor_index;
      ...
    };
    
    void* Init(TfLiteOpaqueContext* context,
        const char* buffer, size_t length) {
      auto* op_data = new MyOpData{};
      ...
      return op_data;
    }
    void Free(TfLiteOpaqueContext* context, void* buffer) {
      ...
      delete reinterpret_cast<MyOpData*>(buffer);
    }
    TfLiteStatus Prepare(TfLiteOpaqueContext* context,
                         TfLiteOpaqueNode* node) {
      ...
      auto* op_data =
          reinterpret_cast<MyOpData*>(TfLiteOpaqueNodeGetUserData(node));
      const int num_temporaries = 1;
      int temporary_tensor_indices[num_temporaries];
      TfLiteOpaqueTensorBuilder* builder = TfLiteOpaqueTensorBuilderCreate();
      TfLiteOpaqueTensorBuilderSetType(builder, kTfLiteFloat32);
      TfLiteOpaqueTensorBuilderSetAllocationType(builder, kTfLiteArenaRw);
      TfLiteOpaqueContextAddTensor(context, builder,
          &temporary_tensor_indices[0]);
      TfLiteOpaqueTensorBuilderDelete(builder);
      TfLiteOpaqueNodeSetTemporaries(node, temporary_tensor_indices,
          num_temporaries);
      op_data->temp_tensor_index = temporary_tensor_indices[0];
      ...
      return kTfLiteOk;
    }
    TfLiteStatus Invoke(TfLiteOpaqueContext* context,
                        TfLiteOpaqueNode* node) {
      ...
      auto* op_data = reinterpret_cast<MyOpData*>(
          TfLiteOpaqueNodeGetUserData(node));
      TfLiteOpaqueTensor* temp_tensor =
          TfLiteOpaqueContextGetOpaqueTensor(context,
              op_data->temp_tensor_index);
      TF_LITE_OPAQUE_ENSURE(context,
          TfLiteTensorType(temp_tensor) == kTfLiteFloat32);
      TF_LITE_OPAQUE_ENSURE(context,
          TfLiteTensorGetAllocationType(temp_Tensor) == kTfLiteArenaRw);
      void *temp_data = TfLiteTensorData(temp_tensor);
      TF_LITE_OPAQUE_ENSURE(context, temp_data != nullptr);
      ...
      return kTfLiteOk;
    }
    
  3. 如果這樣做不會浪費過多記憶體,建議您使用靜態固定大小陣列 (或在 Resize 中預先分配的 std::vector),而不要在每次執行時使用動態分配的 std::vector

  4. 請避免將尚未存在的標準程式庫容器範本執行個體化,因為這會影響二進位檔大小。舉例來說,如果您需要作業中沒有其他核心的 std::map,那麼將 std::vector 與直接索引對應搭配使用,或許可以在縮減二進位檔大小的情況下運作。查看其他核心用來取得深入分析 (或詢問) 哪些核心。

  5. 查看 malloc 傳回記憶體的指標。如果這個指標是 nullptr,則不應使用該指標執行任何作業。如果在函式中使用 malloc 且發生錯誤結束事件,請在離開前解除分配記憶體。

  6. 使用 TF_LITE_OPAQUE_ENSURE(context, condition) 檢查特定條件。使用 TF_LITE_OPAQUE_ENSURE 時,您的程式碼不得讓記憶體停擺。也就是說,這些巨集應在分配到任何可能導致流失的資源之前使用。