Biblioteka wbudowanych operatorów LiteRT obsługuje tylko ograniczoną liczbę operatorów TensorFlow, więc nie każdy model można przekonwertować. Więcej informacji znajdziesz w artykule Zgodność z operatorami.
Aby umożliwić konwersję, użytkownicy mogą udostępnić własną implementację niestandardową nieobsługiwanego operatora TensorFlow w LiteRT, zwaną operatorem niestandardowym. Jeśli zamiast tego chcesz połączyć serię nieobsługiwanych (lub obsługiwanych) operatorów TensorFlow w jeden zoptymalizowany operator niestandardowy, zapoznaj się z informacjami o łączeniu operatorów.
Korzystanie z operatorów niestandardowych obejmuje 4 etapy.
Utwórz model TensorFlow. Upewnij się, że zapisany model (lub definicja wykresu) odwołuje się do operatora LiteRT o prawidłowej nazwie.
Konwertowanie na model LiteRT Aby przekonwertować model, musisz ustawić odpowiedni atrybut konwertera LiteRT.
Utwórz i zarejestruj operatora. Dzięki temu środowisko wykonawcze LiteRT wie, jak zmapować operatora i parametry w grafie na wykonywalny kod C/C++.
Przetestuj i sprawdź profil operatora. Jeśli chcesz przetestować tylko operatora niestandardowego, najlepiej utworzyć model zawierający tylko tego operatora i użyć programu benchmark_model.
Przyjrzyjmy się kompleksowemu przykładowi uruchamiania modelu z niestandardowym operatorem tf.atan
(o nazwie Atan
, patrz Tworzenie modelu TensorFlow), który jest obsługiwany w TensorFlow, ale nie w LiteRT.
Operatorem niestandardowym jest np. operator TensorFlow Text. Przykład kodu znajdziesz w samouczku Konwertowanie TF Text na LiteRT.
Przykład: niestandardowy operator Atan
Przyjrzyjmy się przykładowi obsługi operatora TensorFlow, którego nie ma w LiteRT. Załóżmy, że używamy operatora Atan
i budujemy bardzo prosty model funkcji y = atan(x + offset)
, w którym offset
można trenować.
Tworzenie modelu TensorFlow
Poniższy fragment kodu trenuje prosty model TensorFlow. Ten model zawiera tylko operatora niestandardowego o nazwie Atan
, który jest funkcją y = atan(x +
offset)
, gdzie offset
można trenować.
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
Jeśli w tym momencie spróbujesz wygenerować model LiteRT z domyślnymi flagami konwertera, zobaczysz ten komunikat o błędzie:
Error:
error: 'tf.Atan' op is neither a custom op nor a flex op.
Konwertowanie na model LiteRT
Utwórz model LiteRT z operatorami niestandardowymi, ustawiając atrybut konwertera allow_custom_ops
w sposób pokazany poniżej:
converter = tf.lite.TFLiteConverter.from_concrete_functions([atan.get_concrete_function()], atan) converter.allow_custom_ops = True tflite_model = converter.convert()
Jeśli w tym momencie uruchomisz go za pomocą domyślnego interpretera, używając poleceń takich jak:
interpreter = tf.lite.Interpreter(model_content=tflite_model)
interpreter.allocate_tensors()
Nadal będziesz widzieć ten błąd:
Encountered unresolved custom op: Atan.
Utwórz i zarejestruj operatora.
#include "third_party/tensorflow/lite/c/c_api.h"
#include "third_party/tensorflow/lite/c/c_api_opaque.h"
Niestandardowe operatory LiteRT są definiowane za pomocą prostego interfejsu API w czystym języku C, który składa się z nieprzezroczystego typu (TfLiteOperator
) i powiązanych funkcji.
TfLiteOperator
to typ nieprzezroczysty:
typedef struct TfLiteOperator TfLiteOperator;
TfLiteOperator
przechowuje tożsamość operatora i jego implementację.
(Pamiętaj, że operator różni się od operandów, które są przechowywane w węzłach wykresu LiteRT w przypadku węzłów wywołujących operatora).
Instancje tego typu są tworzone za pomocą wywołań funkcji
TfLiteOperatorCreate
i można je usuwać za pomocą wywołań funkcji
TfLiteOperatorDelete
.
Tożsamość operatora jest ustawiana za pomocą parametrów funkcji konstruktora 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.
);
Implementacja operatora może definiować „metody” o tych sygnaturach.
Wszystkie te metody są opcjonalne, ale aby operator mógł zostać prawidłowo oceniony, jego implementacja musi zdefiniować i ustawić (za pomocą funkcji ustawiających) co najmniej metody Prepare
i 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);
Nazwy funkcji (lub prefiksy przestrzeni nazw w przypadku C++) w implementacji operacji nie muszą być zgodne z nazwami funkcji w powyższym fragmencie kodu, ponieważ interfejs API niestandardowych operacji TF Lite będzie używać tylko ich adresów. Zalecamy deklarowanie ich w anonimowej przestrzeni nazw lub jako funkcji statycznych.
Warto jednak dodać nazwę operatora jako przestrzeń nazw lub prefiks do tych nazw funkcji:
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.
Ponieważ jest to interfejs API w języku C, te „metody” są implementowane jako wskaźniki funkcji w języku C w typie TfLiteOperator
, które są ustawiane przez przekazywanie adresów funkcji implementacji do odpowiednich funkcji ustawiających 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));
Szczegółowe informacje o wartościach TfLiteContext
i TfLiteNode
znajdziesz w artykule common.h
. TfLiteContext
udostępnia funkcje raportowania błędów i dostęp do obiektów globalnych, w tym wszystkich tensorów.
Dyrektywa TfLiteNode
umożliwia implementacjom operatorów dostęp do ich danych wejściowych i wyjściowych.
Gdy interpreter wczytuje model, wywołuje metodę Init()
raz dla każdego węzła w grafie. Dany element Init()
zostanie wywołany więcej niż raz, jeśli operacja jest używana w grafie wiele razy. W przypadku operacji niestandardowych udostępniany jest bufor konfiguracji zawierający flexbuffer, który mapuje nazwy parametrów na ich wartości. W przypadku wbudowanych operacji bufor jest pusty, ponieważ interpreter przeanalizował już parametry operacji. Implementacje jądra, które wymagają stanu, powinny go tutaj zainicjować i przekazać własność wywołującemu. Każdemu wywołaniu Init()
będzie odpowiadać wywołanie Free()
, co umożliwi implementacjom zwolnienie bufora, który mogły przydzielić w Init()
.
Za każdym razem, gdy rozmiar tensorów wejściowych zostanie zmieniony, interpreter przejdzie przez graf, powiadamiając implementacje o zmianie. Dzięki temu mogą zmienić rozmiar wewnętrznego bufora, sprawdzić prawidłowość kształtów i typów danych wejściowych oraz ponownie obliczyć kształty danych wyjściowych. Odbywa się to za pomocą metody Prepare()
, a implementacje mogą uzyskiwać dostęp do swojego stanu za pomocą metody TfLiteOpaqueNodeGetUserData(node)
.
Za każdym razem, gdy uruchamiane jest wnioskowanie, interpreter przechodzi przez graf, wywołując metodę Invoke()
, w której stan jest dostępny jako TfLiteOpaqueNodeGetUserData(node)
.
Operacje niestandardowe można zaimplementować, definiując funkcje „metody”, a następnie definiując funkcję, która zwraca instancję TfLiteOperator
utworzoną przez wywołanie TfLiteOperatorCreate
, a potem odpowiednich metod ustawiających:
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; }
Pamiętaj, że rejestracja nie jest automatyczna i należy jawnie wywołać funkcję MyCustomOperator
(szczegóły poniżej). Standardowy plik BuiltinOpResolver
(dostępny w pliku docelowym :builtin_ops
) zajmuje się rejestracją wbudowanych funkcji, ale operacje niestandardowe trzeba będzie zbierać w osobnych bibliotekach niestandardowych.
Definiowanie jądra w środowisku wykonawczym LiteRT
Aby użyć operacji w LiteRT, wystarczy zdefiniować 2 funkcje (Prepare
i Eval
) oraz trzecią do utworzenia 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; }
Podczas inicjowania OpResolver
dodaj niestandardową operację do modułu rozpoznawania (przykład znajdziesz poniżej). Spowoduje to zarejestrowanie operatora w LiteRT, aby LiteRT mógł używać nowej implementacji.
Zarejestruj operatora w bibliotece jądra.
Teraz musimy zarejestrować operatora w bibliotece jądra. Służy do tego OpResolver
. W tle interpreter wczytuje bibliotekę jąder, które będą przypisane do wykonywania poszczególnych operatorów w modelu.
Domyślna biblioteka zawiera tylko wbudowane jądra, ale można ją zastąpić lub rozszerzyć o niestandardową bibliotekę operatorów.
Klasa OpResolver
, która tłumaczy kody i nazwy operatorów na rzeczywisty kod, jest zdefiniowana w ten sposób:
class OpResolver {
public:
virtual TfLiteRegistration* FindOp(tflite::BuiltinOperator op) const = 0;
virtual TfLiteRegistration* FindOp(const char* op) const = 0;
...
};
Pamiętaj, że ze względu na zgodność wsteczną ta klasa używa starszego typu konkretnego TfLiteRegistration
zamiast typu nieprzezroczystego TfLiteOperator
, ale struktura TfLiteRegistration
zawiera pole registration_external
typu TfLiteOperator*
.
Klasy MutableOpResolver
i BuiltinOpResolver
pochodzą z klasy 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.
};
Zwykłe użycie (bez operacji niestandardowych) wymaga użycia BuiltinOpResolver
i napisania:
tflite::ops::builtin::BuiltinOpResolver resolver;
Aby dodać utworzoną powyżej operację niestandardową, możesz zamiast tego użyć znaku MutableOpResolver
i wywołać funkcję tflite::AddOp
(zanim przekażesz moduł rozpoznawania do funkcji InterpreterBuilder
):
tflite::ops::builtin::MutableOpResolver resolver;
resolver.AddAll(tflite::ops::builtin::BuiltinOpResolver());
tflite::AddOp(&resolver, AtanOpRegistration());
Jeśli zestaw wbudowanych operacji jest zbyt duży, na podstawie danego podzbioru operacji, być może tylko tych zawartych w danym modelu, można wygenerować nowy OpResolver
. Jest to odpowiednik selektywnej rejestracji w TensorFlow (a jej uproszczona wersja jest dostępna w katalogu tools
).
Jeśli chcesz zdefiniować własne operatory niestandardowe w języku Java, musisz obecnie utworzyć własną niestandardową warstwę JNI i skompilować własny plik AAR w tym kodzie JNI. Podobnie, jeśli chcesz zdefiniować te operatory dostępne w Pythonie, możesz umieścić rejestracje w kodzie otoki Pythona.
Pamiętaj, że podobny proces można zastosować w przypadku obsługi zestawu operacji zamiast pojedynczego operatora. Dodaj tyle operatorów AddCustom
, ile potrzebujesz. Dodatkowo MutableOpResolver
umożliwia zastępowanie implementacji wbudowanych funkcji za pomocą AddBuiltin
.
Testowanie i profilowanie operatora
Aby profilować operację za pomocą narzędzia testowego LiteRT, możesz użyć narzędzia do testowania modelu
LiteRT. Na potrzeby testowania możesz sprawić, że lokalna kompilacja LiteRT będzie rozpoznawać Twoją niestandardową operację, dodając odpowiednie wywołanie (jak pokazano powyżej) do pliku register.cc.AddCustom
Sprawdzone metody
Optymalizuj alokacje i dealokacje pamięci z zachowaniem ostrożności. Przydzielanie pamięci w
Prepare
jest bardziej wydajne niż wInvoke
, a przydzielanie pamięci przed pętlą jest lepsze niż w każdej iteracji. Używaj tymczasowych danych tensorów zamiast samodzielnie przydzielać pamięć (patrz punkt 2). W miarę możliwości używaj wskaźników lub odwołań zamiast kopiowania.Jeśli struktura danych będzie utrzymywana przez cały czas trwania operacji, zalecamy wstępne przydzielenie pamięci za pomocą tensorów tymczasowych. Aby odwoływać się do indeksów tensora w innych funkcjach, może być konieczne użycie struktury OpData. Zobacz przykład w jądrze splotu. Poniżej znajdziesz przykładowy fragment kodu.
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; }
Jeśli nie powoduje to zbyt dużego marnowania pamięci, lepiej używać statycznej tablicy o stałym rozmiarze (lub wstępnie przydzielonej pamięci
std::vector
wResize
) niż dynamicznie przydzielanej pamięcistd::vector
w każdej iteracji wykonania.Unikaj tworzenia instancji szablonów kontenerów biblioteki standardowej, które jeszcze nie istnieją, ponieważ wpływają one na rozmiar pliku binarnego. Jeśli na przykład w operacji potrzebujesz elementu
std::map
, który nie występuje w innych jądrach, możesz użyć elementustd::vector
z mapowaniem indeksowania bezpośredniego, zachowując mały rozmiar binarny. Sprawdź, jakich jąder używają inne osoby, aby uzyskać informacje (lub zadać pytanie).Sprawdź wskaźnik do pamięci zwrócony przez funkcję
malloc
. Jeśli ten wskaźnik ma wartośćnullptr
, nie należy wykonywać żadnych operacji przy jego użyciu. Jeślimalloc
w funkcji wystąpi błąd i nastąpi wyjście, przed wyjściem zwolnij pamięć.Użyj aplikacji
TF_LITE_OPAQUE_ENSURE(context, condition)
, aby sprawdzić, czy występuje określony stan. Gdy używane jest makroTF_LITE_OPAQUE_ENSURE
, kod nie może pozostawiać w pamięci nieużywanych danych. Oznacza to, że te makra powinny być używane przed przydzieleniem zasobów, które mogą powodować wyciek pamięci.