Operatory niestandardowe

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.

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 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);

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 TfLiteOperatorSetMethodName:

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 TfLiteContextTfLiteNode 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 (PrepareEval) 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 MutableOpResolverBuiltinOpResolver 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

  1. Optymalizuj alokacje i dealokacje pamięci z zachowaniem ostrożności. Przydzielanie pamięci w Prepare jest bardziej wydajne niż w Invoke, 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.

  2. 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;
    }
    
  3. 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::vectorResize) niż dynamicznie przydzielanej pamięci std::vector w każdej iteracji wykonania.

  4. 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ć elementu std::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).

  5. 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śli malloc w funkcji wystąpi błąd i nastąpi wyjście, przed wyjściem zwolnij pamięć.

  6. Użyj aplikacji TF_LITE_OPAQUE_ENSURE(context, condition), aby sprawdzić, czy występuje określony stan. Gdy używane jest makro TF_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.