Karena library operator bawaan LiteRT hanya mendukung sejumlah kecil operator TensorFlow, tidak semua model dapat dikonversi. Untuk mengetahui detailnya, lihat kompatibilitas operator.
Untuk mengizinkan konversi, pengguna dapat memberikan penerapan kustom mereka sendiri dari operator TensorFlow yang tidak didukung di LiteRT, yang dikenal sebagai operator kustom. Jika Anda ingin menggabungkan serangkaian operator TensorFlow yang tidak didukung (atau didukung) menjadi satu operator kustom yang dioptimalkan dan digabungkan, lihat penggabungan operator.
Penggunaan operator kustom terdiri dari empat langkah.
Buat Model TensorFlow. Pastikan SavedModel (atau Graph Def) merujuk ke operator LiteRT yang diberi nama dengan benar.
Konversi ke Model LiteRT. Pastikan Anda menetapkan atribut konverter LiteRT yang tepat agar model berhasil dikonversi.
Buat dan daftarkan operator. Hal ini dilakukan agar runtime LiteRT mengetahui cara memetakan operator dan parameter dalam grafik Anda ke kode C/C++ yang dapat dieksekusi.
Uji dan buat profil operator Anda. Jika Anda ingin menguji operator kustom saja, sebaiknya buat model dengan operator kustom saja dan gunakan program benchmark_model.
Mari kita bahas contoh menyeluruh menjalankan model dengan operator
kustom tf.atan
(bernama Atan
, lihat Membuat Model TensorFlow.) yang
didukung di TensorFlow, tetapi tidak didukung di LiteRT.
Operator TensorFlow Text adalah contoh operator kustom. Lihat tutorial Mengonversi TF Text ke LiteRT untuk contoh kode.
Contoh: Operator Atan
kustom
Mari kita pelajari contoh dukungan untuk operator TensorFlow yang tidak dimiliki LiteRT. Asumsikan kita menggunakan operator Atan
dan
kita sedang membangun model yang sangat sederhana untuk fungsi y = atan(x + offset)
, dengan
offset
dapat dilatih.
Membuat Model TensorFlow
Cuplikan kode berikut melatih model TensorFlow sederhana. Model ini hanya
berisi operator kustom bernama Atan
, yang merupakan fungsi y = atan(x +
offset)
, dengan offset
dapat dilatih.
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
Pada tahap ini, jika Anda mencoba membuat model LiteRT dengan tanda konverter default, Anda akan mendapatkan pesan error berikut:
Error:
error: 'tf.Atan' op is neither a custom op nor a flex op.
Mengonversi ke Model LiteRT
Buat model LiteRT dengan operator kustom, dengan menetapkan atribut
konverter allow_custom_ops
seperti yang ditunjukkan di bawah:
converter = tf.lite.TFLiteConverter.from_concrete_functions([atan.get_concrete_function()], atan) converter.allow_custom_ops = True tflite_model = converter.convert()
Pada titik ini, jika Anda menjalankannya dengan interpreter default menggunakan perintah seperti berikut:
interpreter = tf.lite.Interpreter(model_content=tflite_model)
interpreter.allocate_tensors()
Anda akan tetap mendapatkan error:
Encountered unresolved custom op: Atan.
Buat dan daftarkan operator.
#include "third_party/tensorflow/lite/c/c_api.h"
#include "third_party/tensorflow/lite/c/c_api_opaque.h"
Operator kustom LiteRT ditentukan menggunakan API C murni sederhana yang terdiri dari jenis buram (TfLiteOperator
) dan fungsi terkait.
TfLiteOperator
adalah jenis buram:
typedef struct TfLiteOperator TfLiteOperator;
TfLiteOperator
menyimpan identitas dan penerapan operator.
(Perhatikan bahwa operator berbeda dari operandnya, yang disimpan di
node grafik LiteRT untuk node yang memanggil operator.)
Instance jenis ini dibuat dengan panggilan ke
TfLiteOperatorCreate
dan dapat dihancurkan dengan memanggil
TfLiteOperatorDelete
.
Identitas operator ditetapkan melalui parameter ke fungsi konstruktor
TfLiteOperatorCreate
:
TfLiteOperator*
TfLiteOperatorCreate(
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.
);
Implementasi operator dapat menentukan "metode" dengan tanda tangan berikut.
Semua metode ini bersifat opsional, tetapi agar operator berhasil dievaluasi, penerapan operator harus menentukan dan menetapkan (menggunakan fungsi setter) setidaknya metode Prepare
dan Invoke
.
// 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);
Nama fungsi (atau awalan namespace, untuk C++) dalam penerapan op Anda tidak harus cocok dengan nama fungsi dalam cuplikan kode di atas, karena API op kustom TF Lite hanya akan menggunakan alamatnya. Sebaiknya Anda mendeklarasikannya di namespace anonim atau sebagai fungsi statis.
Namun, sebaiknya sertakan nama operator Anda sebagai namespace atau awalan pada nama fungsi ini:
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.
Karena ini adalah C API, "metode" ini diimplementasikan sebagai pointer fungsi C dalam
jenis TfLiteOperator
, yang ditetapkan dengan meneruskan alamat
fungsi implementasi Anda ke fungsi setter yang sesuai
TfLiteOperatorSet
MethodName:
void TfLiteOperatorSetInit(
TfLiteOperator* operator,
void* (*init)(TfLiteOpaqueContext* context, const char* buffer,
size_t length));
void TfLiteOperatorSetFree(
TfLiteOperator* operator,
void (*free)(TfLiteOpaqueContext* context, void* data));
void TfLiteOperatorSetPrepare(
TfLiteOperator* operator,
TfLiteStatus (*prepare)(TfLiteOpaqueContext* context,
TfLiteOpaqueNode* node));
void TfLiteOperatorSetInvoke(
TfLiteOperator* operator,
TfLiteStatus (*invoke)(TfLiteOpaqueContext* context,
TfLiteOpaqueNode* node));
void TfLiteOperatorSetAsyncKernel(
TfLiteOperator* operator,
struct TfLiteAsyncKernel* (*async_kernel)(TfLiteOpaqueContext* context,
TfLiteOpaqueNode* node));
Lihat
common.h
untuk mengetahui detail tentang TfLiteContext
dan TfLiteNode
. TfLiteContext
menyediakan fasilitas pelaporan error dan akses ke objek global, termasuk semua tensor.
TfLiteNode
memungkinkan implementasi operator mengakses input dan outputnya.
Saat memuat model, interpreter akan memanggil metode Init()
sekali untuk setiap
node dalam grafik. Init()
tertentu akan dipanggil lebih dari sekali jika op digunakan beberapa kali dalam grafik. Untuk operasi kustom, buffer konfigurasi akan disediakan, yang berisi flexbuffer yang memetakan nama parameter ke nilainya. Buffer
kosong untuk operasi bawaan karena interpreter telah mem-parsing
parameter operasi. Implementasi kernel yang memerlukan status harus menginisialisasinya di sini dan mentransfer kepemilikan ke pemanggil. Untuk setiap panggilan Init()
, akan ada
panggilan yang sesuai ke Free()
, sehingga memungkinkan penerapan untuk membuang
buffer yang mungkin telah dialokasikan di Init()
.
Setiap kali tensor input diubah ukurannya, interpreter akan memproses
grafik yang memberi tahu penerapan perubahan. Hal ini memberi mereka kesempatan untuk
mengubah ukuran buffer internal, memeriksa validitas bentuk dan jenis input, serta
menghitung ulang bentuk output. Semua ini dilakukan melalui metode Prepare()
, dan
implementasi dapat mengakses statusnya menggunakan
TfLiteOpaqueNodeGetUserData(node)
.
Terakhir, setiap kali inferensi berjalan, interpreter melintasi grafik dengan memanggil metode Invoke()
, dan di sini juga status tersedia sebagai TfLiteOpaqueNodeGetUserData(node)
.
Operasi kustom dapat diterapkan dengan menentukan fungsi "metode" tersebut, lalu menentukan fungsi yang menampilkan instance TfLiteOperator
yang dibuat dengan memanggil TfLiteOperatorCreate
, lalu metode setter yang relevan:
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 TfLiteOperator* MyCustomOperator() { // Singleton instance, intentionally never destroyed. static const TfLiteOperator* my_custom_op = ()[] { TfLiteOperator* r = TfLiteOperatorCreate( kTfLiteBuiltinCustom, "MyCustomOp", /*version=*/ 1); TfLiteOperatorSetInit(r, Init); TfLiteOperatorSetFree(r, Free); TfLiteOperatorSetPrepare(r, Prepare); TfLiteOperatorSetInvoke(r, Eval); return r; }; 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 TfLiteOperator* MyCustomOpCreate() { const TfLiteOperator* r = TfLiteOperatorCreate( kTfLiteBuiltinCustom, "MyCustomOp", /*version=*/ 1); TfLiteOperatorSetInit(r, MyCustomOpInit); TfLiteOperatorSetFree(r, MyCustomOpFree); TfLiteOperatorSetPrepare(r, MyCustomOpPrepare); TfLiteOperatorSetInvoke(r, MyCustomOpEval); return r; } const TfLiteOperator* MyCustomOperator() { // Singleton instance, intentionally never destroyed. static const TfLiteOperator* my_custom_op = MyCustomOpCreate(); return my_custom_op; }
Perhatikan bahwa pendaftaran tidak otomatis dan panggilan eksplisit ke fungsi
MyCustomOperator
Anda harus dilakukan (lihat detail di bawah). Meskipun BuiltinOpResolver
standar (tersedia dari target :builtin_ops
) menangani pendaftaran bawaan, operasi kustom harus dikumpulkan dalam library kustom terpisah.
Menentukan kernel di runtime LiteRT
Yang perlu kita lakukan untuk menggunakan op di LiteRT adalah menentukan dua fungsi
(Prepare
dan Eval
), dan fungsi ketiga untuk membuat TfLiteOperator
:
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<float*>(TfLiteOpaqueTensorData(input)); float* output_data = static_cast<float*>(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 TfLiteOperator* AtanOperator() { // Singleton instance, intentionally never destroyed. static const TfLiteOperator* atan_op = ()[] { auto* r = TfLiteOperatorCreate( kTfLiteBuiltinCustom, "ATAN", /*version=*/ 1); TfLiteOperatorSetPrepare(r, Prepare); TfLiteOperatorSetInvoke(r, Eval); return r; }; 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<float*>(TfLiteOpaqueTensorData(input)); float* output_data = static_cast<float*>(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 TfLiteOperator* AtanOpCreate() { TfLiteOperator* r = TfLiteOperatorCreate( kTfLiteBuiltinCustom, "ATAN", /*version=*/ 1); TfLiteOperatorSetPrepare(r, Prepare); TfLiteOperatorSetInvoke(r, Eval); return r; } const TfLiteOperator* AtanOperator() { // Singleton instance, intentionally never destroyed. static const TfLiteOperator* atan_op = AtanOpCreate(); return atan_op; }
Saat menginisialisasi OpResolver
, tambahkan operasi kustom ke resolver (lihat contoh di bawah). Tindakan ini akan mendaftarkan operator dengan LiteRT sehingga LiteRT dapat menggunakan implementasi baru.
Mendaftarkan operator dengan library kernel
Sekarang kita perlu mendaftarkan operator dengan library kernel. Hal ini dilakukan dengan
OpResolver
. Di balik layar, interpreter akan memuat library kernel yang akan ditetapkan untuk menjalankan setiap operator dalam model.
Meskipun library default hanya berisi kernel bawaan, Anda dapat mengganti/menambahkannya dengan operator op library kustom.
Class OpResolver
, yang menerjemahkan kode dan nama operator menjadi kode sebenarnya, ditentukan seperti ini:
class OpResolver {
public:
virtual TfLiteRegistration* FindOp(tflite::BuiltinOperator op) const = 0;
virtual TfLiteRegistration* FindOp(const char* op) const = 0;
...
};
Perhatikan bahwa untuk kompatibilitas mundur, class ini menggunakan jenis konkret yang lebih lama, yaitu
TfLiteRegistration
, bukan jenis buram TfLiteOperator
,
tetapi struct TfLiteRegistration
berisi kolom registration_external
dengan
jenis TfLiteOperator*
.
Class MutableOpResolver
dan BuiltinOpResolver
berasal dari
OpResolver
:
class MutableOpResolver : public OpResolver {
public:
MutableOpResolver(); // Constructs an initially empty op resolver.
void AddAll(const MutableOpResolver& other);
...
};
class BuiltinOpResolver : public MutableOpResolver {
public:
BuiltinOpResolver(); // Constructs an op resolver with all the builtin ops.
};
Penggunaan reguler (tanpa operasi kustom) mengharuskan Anda menggunakan BuiltinOpResolver
dan menulis:
tflite::ops::builtin::BuiltinOpResolver resolver;
Untuk menambahkan operasi kustom yang dibuat di atas, Anda dapat menggunakan MutableOpResolver
, lalu memanggil tflite::AddOp
(sebelum meneruskan resolver ke InterpreterBuilder
):
tflite::ops::builtin::MutableOpResolver resolver;
resolver.AddAll(tflite::ops::builtin::BuiltinOpResolver());
tflite::AddOp(&resolver, AtanOpRegistration());
Jika set operasi bawaan dianggap terlalu besar, OpResolver
baru dapat
dibuat kode berdasarkan subset operasi tertentu, mungkin hanya yang ada
dalam model tertentu. Ini setara dengan pendaftaran selektif TensorFlow
(dan versi sederhananya tersedia di direktori tools
).
Jika ingin menentukan operator kustom di Java, saat ini Anda harus membangun lapisan JNI kustom sendiri dan mengompilasi AAR sendiri dalam kode JNI ini. Demikian pula, jika Anda ingin menentukan operator ini yang tersedia di Python, Anda dapat menempatkan pendaftaran di kode wrapper Python.
Perhatikan bahwa proses serupa seperti di atas dapat diikuti untuk mendukung serangkaian
operasi, bukan satu operator. Cukup tambahkan operator AddCustom
sebanyak yang Anda butuhkan. Selain itu, MutableOpResolver
juga memungkinkan Anda mengganti
implementasi bawaan menggunakan AddBuiltin
.
Menguji dan membuat profil operator
Untuk memprofilkan operasi dengan alat benchmark LiteRT, Anda dapat menggunakan
alat model benchmark
untuk LiteRT. Untuk tujuan pengujian, Anda dapat membuat build lokal LiteRT mengenali operasi kustom dengan menambahkan panggilan AddCustom
yang sesuai (seperti yang ditunjukkan di atas) ke register.cc
Praktik terbaik
Optimalkan alokasi dan de-alokasi memori dengan hati-hati. Mengalokasikan memori di
Prepare
lebih efisien daripada diInvoke
, dan mengalokasikan memori sebelum loop lebih baik daripada di setiap iterasi. Gunakan data tensor sementara daripada mengalokasikan sendiri (lihat item 2). Gunakan pointer/referensi, bukan menyalin sebanyak mungkin.Jika struktur data akan tetap ada selama seluruh operasi, sebaiknya alokasikan memori terlebih dahulu menggunakan tensor sementara. Anda mungkin perlu menggunakan struct OpData untuk mereferensikan indeks tensor dalam fungsi lain. Lihat contoh di kernel untuk konvolusi. Cuplikan kode contoh ada di bawah.
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; }
Jika tidak terlalu banyak memori yang terbuang, sebaiknya gunakan array ukuran tetap statis (atau
std::vector
yang telah dialokasikan sebelumnya diResize
) daripada menggunakanstd::vector
yang dialokasikan secara dinamis setiap iterasi eksekusi.Hindari membuat instance template penampung library standar yang belum ada, karena akan memengaruhi ukuran biner. Misalnya, jika Anda memerlukan
std::map
dalam operasi yang tidak ada di kernel lain, menggunakanstd::vector
dengan pemetaan pengindeksan langsung dapat berfungsi sekaligus menjaga ukuran biner tetap kecil. Lihat kernel lain yang digunakan untuk mendapatkan insight (atau tanyakan).Periksa pointer ke memori yang ditampilkan oleh
malloc
. Jika penunjuk ini adalahnullptr
, tidak ada operasi yang boleh dilakukan menggunakan penunjuk tersebut. Jika Andamalloc
dalam fungsi dan memiliki error keluar, batalkan alokasi memori sebelum Anda keluar.Gunakan
TF_LITE_OPAQUE_ENSURE(context, condition)
untuk memeriksa kondisi tertentu. Kode Anda tidak boleh membiarkan memori menggantung saatTF_LITE_OPAQUE_ENSURE
digunakan, yaitu, makro ini harus digunakan sebelum resource yang akan bocor dialokasikan.