Panoramica di RenderScript

RenderScript è un framework per l'esecuzione di attività ad alta intensità di calcolo con prestazioni elevate Android. RenderScript è principalmente orientato per l'uso con il calcolo data-parallel, sebbene seriale ne possono trarre vantaggio. Il runtime RenderScript esegue il caricamento in contemporanea funzionano sui processori disponibili su un dispositivo, come CPU multi-core e GPU. Ciò consente a concentrarti sull'espressione degli algoritmi piuttosto che sulla pianificazione del lavoro. RenderScript è particolarmente utile per applicazioni che eseguono elaborazione di immagini, fotografia computazionale o visione artificiale.

Per iniziare a utilizzare RenderScript, ci sono due concetti principali che dovresti comprendere:

  • Il linguaggio stesso è derivato da C99 per la scrittura di risorse di computing ad alte prestazioni le API nel tuo codice. Scrivere un kernel RenderScript descrive e come usarlo per scrivere kernel Compute.
  • L'API di controllo viene utilizzata per gestire la durata delle risorse RenderScript e il controllo dell'esecuzione del kernel. È disponibile in tre linguaggi diversi: Java, C++ in Android NDK e il linguaggio kernel derivato da C99. Utilizzo di RenderScript da codice Java e Single-Source RenderScript descrive la prima e la terza rispettivamente.

Scrittura di un kernel RenderScript

Un kernel RenderScript solitamente risiede in un file .rs nella directory <project_root>/src/rs; ogni file .rs è chiamato script. Ogni script contiene il proprio set di kernel, funzioni e variabili. Uno script può contiene:

  • Una dichiarazione pragma (#pragma version(1)) che dichiara la versione del Linguaggio kernel RenderScript utilizzato in questo script. Attualmente, 1 è l'unico valore valido.
  • Una dichiarazione pragma (#pragma rs java_package_name(com.example.app)) che dichiara il nome pacchetto delle classi Java riflesse da questo script. Tieni presente che il file .rs deve far parte del pacchetto dell'applicazione e non in un progetto libreria.
  • Zero o più funzioni richiamabili. Una funzione richiamabile è un RenderScript a thread singolo funzione che puoi chiamare dal codice Java con argomenti arbitrari. Questi sono spesso utili per configurazione iniziale o calcoli seriali all'interno di una pipeline di elaborazione più ampia.
  • Zero o più script globali. Uno script globale è simile a una variabile globale in C. Puoi gli script di accesso globali dal codice Java e vengono spesso utilizzati per il passaggio di parametri a RenderScript i kernel. I valori globali degli script sono descritti più dettagliatamente qui.

  • Zero o più kernel computing. Un kernel di computing è una funzione o una raccolta di funzioni che puoi chiedere al runtime di RenderScript di eseguire in parallelo attraverso una raccolta di dati. Esistono due tipi di computing kernel: mappatura dei kernel (chiamati anche kernel foreach) e dei kernel di riduzione.

    Un kernel di mappatura è una funzione parallela che opera su una raccolta di Allocations delle stesse dimensioni. Per impostazione predefinita, esegue una volta per ogni coordinata in quelle dimensioni. In genere (ma non esclusivamente) è abituata trasformare una raccolta di input Allocations in un genera Allocation uno Element a nel tempo.

    • Ecco un esempio di un semplice kernel di mappatura:

      uchar4 RS_KERNEL invert(uchar4 in, uint32_t x, uint32_t y) {
        uchar4 out = in;
        out.r = 255 - in.r;
        out.g = 255 - in.g;
        out.b = 255 - in.b;
        return out;
      }

      Sotto molti aspetti, è identico al modello C standard personalizzata. La proprietà RS_KERNEL applicata alla proprietà prototipo di funzione specifica che la funzione è un kernel di mappatura RenderScript anziché un funzione richiamabile. L'argomento in viene compilato automaticamente in base al input Allocation passato al lancio del kernel. La gli argomenti x e y sono descritti di seguito. Il valore restituito dal kernel è vengono scritti automaticamente nella posizione appropriata nell'output Allocation. Per impostazione predefinita, questo kernel viene eseguito su tutto il suo input Allocation, con un'esecuzione della funzione kernel per Element in Allocation.

      Un kernel di mappatura può avere uno o più Allocations di input, un singolo output Allocation o entrambi. La Il runtime di RenderScript controlla che tutte le allocazioni di input e output abbiano le stesse dimensioni e che i tipi Element di input e output Le allocazioni corrispondono al prototipo del kernel; Se uno di questi controlli non va a buon fine, RenderScript genera un'eccezione.

      NOTA:prima di Android 6.0 (livello API 23), un kernel di mappatura potrebbe non avere più di un input Allocation.

      Se hai bisogno di più input o output Allocations rispetto a del kernel, questi oggetti devono essere associati a rs_allocation script globali e a cui si accede da un kernel o da una funzione invokable tramite rsGetElementAt_type() o rsSetElementAt_type().

      NOTA: RS_KERNEL è una macro definiti automaticamente da RenderScript per comodità:

      #define RS_KERNEL __attribute__((kernel))
      

    Un kernel di riduzione è una famiglia di funzioni che opera su una raccolta di input Allocations delle stesse dimensioni. Per impostazione predefinita, la sua funzione accumulatore viene eseguita una volta per coordinate in quelle dimensioni. Viene usato in genere (ma non esclusivamente) per "ridurre" un di input Allocations in una singola valore.

    • Ecco un esempio di semplice riduzione kernel che somma Elements del suo input:

      #pragma rs reduce(addint) accumulator(addintAccum)
      
      static void addintAccum(int *accum, int val) {
        *accum += val;
      }

      Un kernel di riduzione è costituito da una o più funzioni scritte dall'utente. #pragma rs reduce viene utilizzato per definire il kernel specificandone il nome (addint, in questo esempio) e i nomi e i ruoli delle funzioni che rendono il kernel (una funzione accumulator addintAccum, in questo esempio). Tutte queste funzioni devono essere static. Un kernel di riduzione è sempre richiede una funzione accumulator; potrebbe anche avere altre funzioni, a seconda su ciò che deve fare il kernel.

      Una funzione di accumulatore del kernel di riduzione deve restituire void e avere almeno due argomenti. Il primo argomento (accum, in questo esempio) è un puntatore a un elemento dati accumulatore e il secondo (val, in questo esempio) compilato automaticamente in base all'input Allocation passato a il lancio del kernel. L'elemento dati accumulatore viene creato dal runtime RenderScript; di predefinito, è inizializzato su zero. Per impostazione predefinita, questo kernel viene eseguito su tutto il suo input Allocation, con un'esecuzione della funzione di accumulatore per Element in Allocation. Di predefinito, il valore finale dell'elemento dati accumulatore viene trattato come il risultato e viene restituito a Java. Il runtime di RenderScript verifica che il tipo Element dell'allocazione di input corrisponda alla funzione accumulatore prototipo; se non corrisponde, RenderScript genera un'eccezione.

      Un kernel di riduzione ha uno o più input Allocations ma nessun output Allocations.

      I kernel di riduzione sono spiegati in modo più dettagliato qui.

      I kernel di riduzione sono supportati in Android 7.0 (livello API 24) e versioni successive.

    Una funzione kernel di mappatura o una funzione di accumulatore del kernel di riduzione può accedere alle coordinate dell'esecuzione corrente utilizzando gli argomenti speciali x, y e z, che devono essere di tipo int o uint32_t. Questi argomenti sono facoltativi.

    Una funzione kernel di mappatura o un accumulatore di riduzione del kernel può anche accettare l'argomento speciale facoltativo context di tipo rs_kernel_context. È necessaria per una famiglia di API runtime utilizzate per eseguire query alcune proprietà dell'esecuzione corrente, ad esempio rsGetDimX. L'argomento context è disponibile in Android 6.0 (livello API 23) e versioni successive.

  • Una funzione init() facoltativa. La funzione init() è un tipo speciale di funzione invokable eseguita da RenderScript quando viene creata un'istanza dello script per la prima volta. Ciò consente automaticamente al momento della creazione dello script.
  • Zero o più funzioni e globali degli script statici. Uno script statico globale equivale a uno script globale, ad eccezione del fatto che non è accessibile dal codice Java. Una funzione statica è uno standard funzione che può essere chiamata da qualsiasi kernel o funzione invokable nello script, ma che non è esposta all'API Java. Se non è necessario accedere a uno script globale o a una funzione dal codice Java, ti consigliamo vivamente di dichiararlo static.

Impostazione della precisione con rappresentazione in virgola mobile

Puoi controllare il livello richiesto di precisione con rappresentazione in virgola mobile in uno script. Ciò è utile se non è richiesto lo standard IEEE 754-2008 (utilizzato per impostazione predefinita). I seguenti pragmi possono impostare un un diverso livello di precisione con rappresentazione in virgola mobile:

  • #pragma rs_fp_full (valore predefinito se non viene specificato nulla): per le app che richiedono la precisione in virgola mobile come descritto dallo standard IEEE 754-2008.
  • #pragma rs_fp_relaxed: per le app che non richiedono lo standard IEEE 754-2008. la conformità e può tollerare una precisione minore. Questa modalità abilita il flush-to-zero per i denorm arrotondato a zero.
  • #pragma rs_fp_imprecise: per le app che non hanno una precisione rigorosa i tuoi requisiti. Questa modalità attiva tutti gli elementi in rs_fp_relaxed, insieme ai seguenti:
    • Le operazioni che restituiscono -0,0 possono invece restituire +0,0.
    • Le operazioni su INF e NAN non sono definite.

La maggior parte delle applicazioni può utilizzare rs_fp_relaxed senza effetti collaterali. Questo potrebbe essere molto vantaggiosa su alcune architetture grazie a ottimizzazioni aggiuntive disponibili solo con e precisione (ad esempio le istruzioni per la CPU SIMD).

Accesso alle API RenderScript da Java

Quando sviluppi un'applicazione Android che utilizza RenderScript, puoi accedere alla relativa API da Java in uno dei due modi seguenti:

  • android.renderscript - Le API di questo pacchetto della classe sono disponibile sui dispositivi con Android 3.0 (livello API 11) e versioni successive.
  • android.support.v8.renderscript - Le API di questo pacchetto sono disponibile tramite un team di assistenza , che consente di utilizzarli sui dispositivi con Android 2.3 (livello API 9) e in alto.

Ecco i compromessi:

  • Se utilizzi le API Support Library, la parte RenderScript dell'applicazione verrà compatibile con dispositivi con Android 2.3 (livello API 9) e versioni successive, indipendentemente dal RenderScript le funzionalità che utilizzi. In questo modo l'applicazione può funzionare su più dispositivi rispetto all'utilizzo della native (android.renderscript).
  • Alcune funzionalità di RenderScript non sono disponibili tramite le API Support Library.
  • Se usi le API Support Library, otterrai APK più grandi rispetto a quelli se utilizzi le API native (android.renderscript).

Utilizzo delle API della libreria di supporto RenderScript

Per utilizzare le API Support Library RenderScript, devi configurare la tua per potervi accedere. Per l'utilizzo sono necessari i seguenti strumenti SDK Android queste API:

  • Android SDK Tools versione 22.2 o successive
  • SDK Build-tools per Android versione 18.1.0 o successiva

Tieni presente che a partire da SDK per Android Build-tools 24.0.0, Android 2.2 (livello API 8) non è più supportato.

Puoi controllare e aggiornare la versione installata di questi strumenti nel Gestione SDK Android.

Per utilizzare le API Support Library RenderScript:

  1. Assicurati di avere installato la versione richiesta dell'SDK Android.
  2. Aggiorna le impostazioni per il processo di compilazione di Android per includere le impostazioni di RenderScript:
    • Apri il file build.gradle nella cartella dell'app del modulo dell'applicazione.
    • Aggiungi le seguenti impostazioni di RenderScript al file:

      Alla moda

              android {
                  compileSdkVersion 33
      
                  defaultConfig {
                      minSdkVersion 9
                      targetSdkVersion 19
      
                      renderscriptTargetApi 18
                      renderscriptSupportModeEnabled true
                  }
              }
              

      Kotlin

              android {
                  compileSdkVersion(33)
      
                  defaultConfig {
                      minSdkVersion(9)
                      targetSdkVersion(19)
      
                      renderscriptTargetApi = 18
                      renderscriptSupportModeEnabled = true
                  }
              }
              

      Le impostazioni elencate sopra controllano un comportamento specifico nel processo di compilazione di Android:

      • renderscriptTargetApi - Specifica la versione bytecode da utilizzare generati. Ti consigliamo di impostare questo valore sul livello API più basso in grado di fornire. tutte le funzionalità che stai usando e imposta renderscriptSupportModeEnabled a true. I valori validi per questa impostazione sono qualsiasi valore intero dall'11 al livello API rilasciato più di recente. Se la versione minima dell'SDK specificato nel file manifest dell'applicazione è impostato su un valore diverso, tale valore viene ignorato e il valore target nel file di build viene utilizzato per impostare il valore minimo Versione SDK.
      • renderscriptSupportModeEnabled - Specifica che l'elemento generato Il bytecode deve tornare a una versione compatibile se il dispositivo è in esecuzione non supporta la versione di destinazione.
  3. Nelle classi delle applicazioni che utilizzano RenderScript, aggiungi un'importazione per la libreria di supporto classi:

    Kotlin

    import android.support.v8.renderscript.*
    

    Java

    import android.support.v8.renderscript.*;
    

Utilizzo di RenderScript da codice Java o Kotlin

L'utilizzo di RenderScript dal codice Java o Kotlin si basa sulle classi API situate nella android.renderscript o il pacchetto android.support.v8.renderscript. Più alta seguono lo stesso modello di utilizzo di base:

  1. Inizializza un contesto RenderScript. Il contesto RenderScript, creato con create(Context), garantisce l'utilizzo di RenderScript e fornisce una per controllare la durata di tutti gli oggetti RenderScript successivi. Dovresti considerare il contesto di un'operazione potenzialmente a lunga esecuzione, dal momento che può creare risorse su diverse componenti hardware; non dovrebbe trovarsi nel percorso critico di un'applicazione, possibile. In genere, un'applicazione ha un solo contesto RenderScript alla volta.
  2. Crea almeno un Allocation da passare a un script. Un Allocation è un oggetto RenderScript che fornisce per una quantità fissa di dati. I kernel negli script richiedono Allocation come input e output e gli oggetti Allocation possono essere a cui si accede nei kernel utilizzando rsGetElementAt_type() e rsSetElementAt_type() se associato come script globali. Gli oggetti Allocation consentono il passaggio di array dal codice Java a RenderScript il codice sorgente e viceversa. Allocation oggetti vengono in genere creati utilizzando createTyped() o createFromBitmap().
  3. Crea gli script necessari. Sono disponibili due tipi di script quando utilizzi RenderScript:
    • ScriptC: si tratta degli script definiti dall'utente, come descritto nella sezione precedente Scrittura di un kernel RenderScript. Ogni script ha una classe Java riportate dal compilatore RenderScript per facilitare l'accesso allo script dal codice Java; questo corso si chiama ScriptC_filename. Ad esempio, se il kernel di mappatura in alto si trovavano in invert.rs e un contesto RenderScript era già presente in mRenderScript, il codice Java o Kotlin per creare un'istanza dello script sarebbe:

      Kotlin

      val invert = ScriptC_invert(renderScript)
      

      Java

      ScriptC_invert invert = new ScriptC_invert(renderScript);
      
    • ScriptIntrinsic: sono kernel RenderScript integrati per le operazioni comuni, come sfocatura gaussiana, convoluzione e fusione delle immagini. Per ulteriori informazioni, vedi le sottoclassi ScriptIntrinsic.
  4. Compilare le allocazioni con i dati. Ad eccezione delle allocazioni create con createFromBitmap(), un'allocazione viene compilata con dati vuoti quando creato per la prima volta. Per compilare un'allocazione, usa uno dei campi "copy" in Allocation. La copia sono sincroni.
  5. Imposta gli script globali necessari. Puoi impostare i valori globali utilizzando i metodi nella stessa classe ScriptC_filename denominata set_globalname. Per Ad esempio, per impostare una variabile int denominata threshold, utilizza la Metodo Java set_threshold(int); e per impostare una variabile rs_allocation denominata lookup, utilizza la set_lookup(Allocation). I metodi set sono asincroni.
  6. Avvia i kernel appropriati e le funzioni richiamabili.

    I metodi per lanciare un determinato kernel sono nella stessa classe ScriptC_filename con metodi denominati forEach_mappingKernelName() o reduce_reductionKernelName(). Questi lanci sono asincroni. In base agli argomenti al kernel, la classe prende una o più allocazioni, che devono avere tutte le stesse dimensioni. Per impostazione predefinita, il kernel viene eseguito su ogni coordinata in quelle dimensioni; eseguire un kernel su un sottoinsieme di queste coordinate, passa un Script.LaunchOptions appropriato come ultimo argomento al metodo forEach o reduce.

    Avvia le funzioni richiamabili utilizzando i metodi invoke_functionName nella stessa classe ScriptC_filename. Questi lanci sono asincroni.

  7. Recupera i dati da Allocation oggetti e javaFutureType. Per accedere ai dati di un Allocation da codice Java, devi copiare questi dati in Java usando uno dei file "copy" in Allocation. Per ottenere il risultato di un kernel di riduzione, devi usare il metodo javaFutureType.get(). La copia mentre i metodi get() sono sincroni.
  8. Riduci il contesto di RenderScript. Puoi eliminare il contesto di RenderScript con destroy() o consentendo il contesto RenderScript oggetto di garbage collection. Questo determina un ulteriore utilizzo di qualsiasi oggetto appartenente a contesto per generare un'eccezione.

Modello di esecuzione asincrono

forEach, invoke, reduce, e set sono asincroni; ognuno può tornare a Java prima di completare azione richiesta. Tuttavia, le singole azioni vengono serializzate nell'ordine in cui vengono lanciate.

Il corso Allocation fornisce "testo" per copiare i dati e dalle allocazioni. Una "copia" è sincrono ed è serializzato rispetto a qualsiasi delle azioni asincrone precedenti che riguardano la stessa allocazione.

Le classi javaFutureType riflesse forniscono un metodo get() per ottenere il risultato di una riduzione. get() è sincrona ed è serializzata rispetto alla riduzione (che è asincrona).

RenderScript a origine singola

Android 7.0 (livello API 24) introduce una nuova funzionalità di programmazione chiamata Sorgente singola RenderScript, in cui i kernel vengono avviati dallo script in cui sono definiti, anziché da Java. Questo approccio è attualmente limitato ai kernel di mappatura, denominati semplicemente "kernel" in questa sezione per la concisione. Questa nuova funzionalità supporta anche la creazione di allocazioni di tipo rs_allocation dall'interno dello script. Ora è possibile implementare un intero algoritmo esclusivamente all'interno di uno script, anche se sono necessari più avvii del kernel. Il vantaggio è duplice: un codice più leggibile, perché mantiene l'implementazione di un algoritmo in una sola lingua; e potenzialmente più veloce per il codice, grazie al minor numero di transizioni tra Java e RenderScript su più avvii del kernel.

In RenderScript a origine singola, scrivi i kernel come descritto in Scrittura di un kernel RenderScript. Quindi scrivi una funzione richiamabile che chiama rsForEach() per avviarle. L'API prende una funzione kernel come prima seguito dalle allocazioni di input e output. Un'API simile rsForEachWithOptions() accetta un argomento aggiuntivo di tipo rs_script_call_t, che specifica un sottoinsieme degli elementi dell'input e allocazioni dell'output affinché la funzione kernel sia elaborata.

Per avviare il calcolo di RenderScript, richiama la funzione invokable da Java. Segui i passaggi descritti in Utilizzare RenderScript dal codice Java. Nel passaggio per lanciare i kernel appropriati, chiama la funzione invokable utilizzando invoke_function_name(), che avvia la l'intero calcolo, incluso l'avvio dei kernel.

Spesso sono necessarie allocazioni per salvare i risultati intermedi del lancio di un kernel all'altro. Puoi crearli utilizzando rsCreateAllocation(). Una forma facile da usare di questa API è rsCreateAllocation_<T><W>(…), dove T è il tipo di dati per e W è la larghezza vettoriale dell'elemento. L'API prende le dimensioni dimensioni X, Y e Z come argomenti. Per le allocazioni 1D o 2D, la dimensione della dimensione Y o Z può omettere. Ad esempio, rsCreateAllocation_uchar4(16384) crea un'allocazione 1D di 16384 elementi, ognuno dei quali è del tipo uchar4.

Le allocazioni vengono gestite automaticamente dal sistema. Tu non devi rilasciarle o liberarle esplicitamente. Tuttavia, puoi chiamare rsClearObject(rs_allocation* alloc) per indicare che non hai più bisogno dell'handle alloc all'allocazione sottostante, in modo che il sistema possa liberare risorse il prima possibile.

La sezione Scrittura di un kernel RenderScript contiene un esempio che inverte un'immagine. L'esempio seguente espande questo testo per applicare più di un effetto a un'immagine, usando Single-Source RenderScript. Include un altro kernel, greyscale, che trasforma un l'immagine a colori in bianco e nero. Una funzione richiamabile process() applica quindi i due kernel consecutivamente a un'immagine di input e produce un'immagine di output. Allocazioni sia per l'input gli output vengono passati come argomenti di tipo rs_allocation.

// File: singlesource.rs

#pragma version(1)
#pragma rs java_package_name(com.android.rssample)

static const float4 weight = {0.299f, 0.587f, 0.114f, 0.0f};

uchar4 RS_KERNEL invert(uchar4 in, uint32_t x, uint32_t y) {
  uchar4 out = in;
  out.r = 255 - in.r;
  out.g = 255 - in.g;
  out.b = 255 - in.b;
  return out;
}

uchar4 RS_KERNEL greyscale(uchar4 in) {
  const float4 inF = rsUnpackColor8888(in);
  const float4 outF = (float4){ dot(inF, weight) };
  return rsPackColorTo8888(outF);
}

void process(rs_allocation inputImage, rs_allocation outputImage) {
  const uint32_t imageWidth = rsAllocationGetDimX(inputImage);
  const uint32_t imageHeight = rsAllocationGetDimY(inputImage);
  rs_allocation tmp = rsCreateAllocation_uchar4(imageWidth, imageHeight);
  rsForEach(invert, inputImage, tmp);
  rsForEach(greyscale, tmp, outputImage);
}

Puoi chiamare la funzione process() da Java o Kotlin nel seguente modo:

Kotlin

val RS: RenderScript = RenderScript.create(context)
val script = ScriptC_singlesource(RS)
val inputAllocation: Allocation = Allocation.createFromBitmapResource(
        RS,
        resources,
        R.drawable.image
)
val outputAllocation: Allocation = Allocation.createTyped(
        RS,
        inputAllocation.type,
        Allocation.USAGE_SCRIPT or Allocation.USAGE_IO_OUTPUT
)
script.invoke_process(inputAllocation, outputAllocation)

Java

// File SingleSource.java

RenderScript RS = RenderScript.create(context);
ScriptC_singlesource script = new ScriptC_singlesource(RS);
Allocation inputAllocation = Allocation.createFromBitmapResource(
    RS, getResources(), R.drawable.image);
Allocation outputAllocation = Allocation.createTyped(
    RS, inputAllocation.getType(),
    Allocation.USAGE_SCRIPT | Allocation.USAGE_IO_OUTPUT);
script.invoke_process(inputAllocation, outputAllocation);

Questo esempio mostra come un algoritmo che coinvolge due lanci del kernel possa essere implementato completamente nel linguaggio RenderScript stesso. Senza origine singola RenderScript, dovresti lanciare entrambi i kernel dal codice Java, separando gli avvii dei kernel dalle definizioni del kernel, rendendo più difficile la comprensione dell'intero algoritmo. Non solo il codice Single-Source RenderScript più facile da leggere, elimina anche la transizione tra Java e lo script all'avvio del kernel. Alcuni algoritmi iterativi possono avviare kernel centinaia di volte, il che rende considerevole l'overhead di questa transizione.

Globali script

Uno script globale è un normale codice non static una variabile globale in un file di script (.rs). Per uno script con nome globale var definito nel filename.rs, ci sarà un evento il metodo get_var si riflette nella nel corso ScriptC_filename. A meno che il livello è const, ci sarà anche metodo set_var.

Un determinato script globale ha due valori separati: Java e un valore script. Questi valori si comportano come segue:

  • Se var ha un inizializzatore statico nello script, specifica il valore iniziale di var sia in Java sia nella lo script. In caso contrario, il valore iniziale è zero.
  • Accede a var all'interno dello script in lettura e scrittura il valore dello script.
  • Il metodo get_var legge le valore.
  • Il metodo set_var (se esistente) scrive il valore Java immediatamente e scrive il valore dello script in modo asincrono.

NOTA:ciò significa che, ad eccezione di eventuali un inizializzatore statico nello script, i valori scritti in un messaggio all'interno di uno script non sono visibili a Java.

Riduzione dei kernel in profondità

Per riduzione si intende il processo di combinazione di una raccolta di dati in un valore. Si tratta di un'utile primitiva nella programmazione parallela, con applicazioni come seguenti:

  • calcolando la somma o il prodotto su tutti i dati
  • operazioni logiche di computing (and, or, xor) su tutti i dati
  • trovando il valore minimo o massimo all'interno dei dati
  • Cercare un valore specifico o la coordinata di un valore specifico all'interno dei dati

In Android 7.0 (livello API 24) e versioni successive, RenderScript supporta i kernel di riduzione per consentire efficienti algoritmi di riduzione scritti dall'utente. Puoi avviare kernel di riduzione sugli input con 1, 2 o 3 dimensioni.

Un esempio riportato sopra mostra un semplice kernel di riduzione addint. Ecco un kernel di riduzione findMinAndMax più complicato che trova le posizioni dei valori minimo e massimo di long in una Allocation monodimensionale:

#define LONG_MAX (long)((1UL << 63) - 1)
#define LONG_MIN (long)(1UL << 63)

#pragma rs reduce(findMinAndMax) \
  initializer(fMMInit) accumulator(fMMAccumulator) \
  combiner(fMMCombiner) outconverter(fMMOutConverter)

// Either a value and the location where it was found, or INITVAL.
typedef struct {
  long val;
  int idx;     // -1 indicates INITVAL
} IndexedVal;

typedef struct {
  IndexedVal min, max;
} MinAndMax;

// In discussion below, this initial value { { LONG_MAX, -1 }, { LONG_MIN, -1 } }
// is called INITVAL.
static void fMMInit(MinAndMax *accum) {
  accum->min.val = LONG_MAX;
  accum->min.idx = -1;
  accum->max.val = LONG_MIN;
  accum->max.idx = -1;
}

//----------------------------------------------------------------------
// In describing the behavior of the accumulator and combiner functions,
// it is helpful to describe hypothetical functions
//   IndexedVal min(IndexedVal a, IndexedVal b)
//   IndexedVal max(IndexedVal a, IndexedVal b)
//   MinAndMax  minmax(MinAndMax a, MinAndMax b)
//   MinAndMax  minmax(MinAndMax accum, IndexedVal val)
//
// The effect of
//   IndexedVal min(IndexedVal a, IndexedVal b)
// is to return the IndexedVal from among the two arguments
// whose val is lesser, except that when an IndexedVal
// has a negative index, that IndexedVal is never less than
// any other IndexedVal; therefore, if exactly one of the
// two arguments has a negative index, the min is the other
// argument. Like ordinary arithmetic min and max, this function
// is commutative and associative; that is,
//
//   min(A, B) == min(B, A)               // commutative
//   min(A, min(B, C)) == min((A, B), C)  // associative
//
// The effect of
//   IndexedVal max(IndexedVal a, IndexedVal b)
// is analogous (greater . . . never greater than).
//
// Then there is
//
//   MinAndMax minmax(MinAndMax a, MinAndMax b) {
//     return MinAndMax(min(a.min, b.min), max(a.max, b.max));
//   }
//
// Like ordinary arithmetic min and max, the above function
// is commutative and associative; that is:
//
//   minmax(A, B) == minmax(B, A)                  // commutative
//   minmax(A, minmax(B, C)) == minmax((A, B), C)  // associative
//
// Finally define
//
//   MinAndMax minmax(MinAndMax accum, IndexedVal val) {
//     return minmax(accum, MinAndMax(val, val));
//   }
//----------------------------------------------------------------------

// This function can be explained as doing:
//   *accum = minmax(*accum, IndexedVal(in, x))
//
// This function simply computes minimum and maximum values as if
// INITVAL.min were greater than any other minimum value and
// INITVAL.max were less than any other maximum value.  Note that if
// *accum is INITVAL, then this function sets
//   *accum = IndexedVal(in, x)
//
// After this function is called, both accum->min.idx and accum->max.idx
// will have nonnegative values:
// - x is always nonnegative, so if this function ever sets one of the
//   idx fields, it will set it to a nonnegative value
// - if one of the idx fields is negative, then the corresponding
//   val field must be LONG_MAX or LONG_MIN, so the function will always
//   set both the val and idx fields
static void fMMAccumulator(MinAndMax *accum, long in, int x) {
  IndexedVal me;
  me.val = in;
  me.idx = x;

  if (me.val <= accum->min.val)
    accum->min = me;
  if (me.val >= accum->max.val)
    accum->max = me;
}

// This function can be explained as doing:
//   *accum = minmax(*accum, *val)
//
// This function simply computes minimum and maximum values as if
// INITVAL.min were greater than any other minimum value and
// INITVAL.max were less than any other maximum value.  Note that if
// one of the two accumulator data items is INITVAL, then this
// function sets *accum to the other one.
static void fMMCombiner(MinAndMax *accum,
                        const MinAndMax *val) {
  if ((accum->min.idx < 0) || (val->min.val < accum->min.val))
    accum->min = val->min;
  if ((accum->max.idx < 0) || (val->max.val > accum->max.val))
    accum->max = val->max;
}

static void fMMOutConverter(int2 *result,
                            const MinAndMax *val) {
  result->x = val->min.idx;
  result->y = val->max.idx;
}

NOTA: ci sono altre riduzioni degli esempi qui.

Per eseguire un kernel di riduzione, il runtime di RenderScript crea uno o più denominate dati dell'accumulatore per mantenere lo stato del processo di riduzione. Runtime RenderScript sceglie il numero di elementi dati di accumulatori in modo da massimizzare le prestazioni. Il tipo degli elementi di dati accumulatori (accumType) è determinato dall'accumulatore funzione: il primo argomento di questa funzione è un puntatore a un accumulatore di dati molto utile. Per impostazione predefinita, ogni elemento dati accumulatore viene inizializzato su zero (come se di memset); tuttavia, puoi scrivere una funzione di inizializzazione per fare qualcosa diverso.

Esempio: nel componente aggiuntivo kernel, gli elementi di dati accumulatori (di tipo int) vengono utilizzati per sommare i dati e i relativi valori. Non esiste una funzione di inizializzazione, quindi ogni elemento dati accumulatore viene inizializzato zero.

Esempio: in il kernel findMinAndMax, gli elementi di dati dell'accumulatore (di tipo MinAndMax) vengono utilizzati per tenere traccia dei valori minimo e massimo trovati finora. Esiste una funzione di inizializzazione per impostarli su LONG_MAX e LONG_MIN, rispettivamente; e di impostare le posizioni di questi valori su -1, per indicare che i valori non sono effettivamente presenti nella parte (vuota) dell'input che è stata elaborati.

RenderScript chiama la funzione di accumulatore una volta per ogni coordinata nella input/i. Tipicamente, la funzione dovrebbe aggiornare in qualche modo l'elemento dati accumulatore in base all'input.

Esempio: nel componente aggiuntivo kernel, la funzione accumulatore aggiunge il valore di un elemento di input all'accumulatore dati.

Esempio: in il kernel findMinAndMax, la funzione di accumulatore verifica se il valore di un elemento di input è inferiore o uguale al valore minimo registrato nella voce di dati accumulatori e/o maggiore o uguale al valore massimo valore registrato nella voce di dati accumulatore e aggiorna quest'ultima di conseguenza.

Dopo che la funzione accumulatore è stata richiamata una volta per ogni coordinata negli ingressi, RenderScript deve combinare l'accumulatore insieme di elementi di dati in un unico elemento dati accumulatore. Puoi scrivere un combiner . Se la funzione di accumulatore ha un singolo input e argomenti speciali, non dovrai scrivere un combinatore funzione; RenderScript utilizzerà la funzione accumulatore per combinare i dati dell'accumulatore elementi. (Puoi comunque scrivere una funzione combinatore se questo comportamento predefinito non corrisponde desiderato.

Esempio: nel componente aggiuntivo kernel, non esiste una funzione combinatore, quindi verrà utilizzata la funzione di accumulatore. Questo è il comportamento corretto, perché se dividiamo un insieme di valori in due parti e somma i valori delle due parti separatamente, la somma delle due somme è la stessa aggiungendo l'intera raccolta.

Esempio: in il kernel findMinAndMax, la funzione combinatore verifica se il valore minimo registrato nella sorgente "source" dati accumulatori l'elemento *val è inferiore al valore minimo registrato nella "destinazione" accumulatore di dati *accum e aggiorna *accum di conseguenza. Funziona in modo simile per il valore massimo. Questa operazione aggiorna *accum allo stato che avrebbe avuto se tutti i valori di input fossero stati accumulati *accum anziché alcuni in *accum e altri in *accum *val.

Dopo aver combinato tutti gli elementi di dati dell'accumulatore, RenderScript determina il risultato della riduzione per tornare a Java. Puoi scrivere un outconverter . Non è necessario scrivere una funzione di outconverter se vuoi il valore finale degli elementi di dati dell'accumulatore combinati risultanti dalla riduzione.

Esempio: nel kernel addint, non esiste una funzione di outconverter. Il valore finale degli elementi di dati combinati è la somma dei tutti gli elementi dell'input, ovvero il valore che vogliamo restituire.

Esempio: in il kernel findMinAndMax, la funzione outconverter inizializza un valore di risultato int2 per contenere le località del minimo e del valori massimi derivanti dalla combinazione di tutti gli elementi di dati dell'accumulatore.

Scrittura di un kernel di riduzione

#pragma rs reduce definisce un kernel di riduzione specificandone il nome, nonché i nomi e i ruoli delle funzioni che rendono per avviare il kernel. Tutte queste funzioni devono essere static. Un kernel di riduzione richiede sempre un accumulator funzione; puoi omettere alcune o tutte le altre funzioni, a seconda di ciò che vuoi che deve fare il kernel.

#pragma rs reduce(kernelName) \
  initializer(initializerName) \
  accumulator(accumulatorName) \
  combiner(combinerName) \
  outconverter(outconverterName)

Il significato degli elementi in #pragma è il seguente:

  • reduce(kernelName) (obbligatorio): specifica che un kernel di riduzione viene in fase di definizione. Un metodo Java riflesso reduce_kernelName avvierà il in un kernel.
  • initializer(initializerName) (facoltativo): specifica il nome del di inizializzazione per questo kernel di riduzione. All'avvio del kernel, RenderScript chiama questa funzione una volta per ogni elemento dati accumulatore. La deve essere definita come segue:

    static void initializerName(accumType *accum) { … }

    accum è un puntatore a un elemento dati accumulatore per questa funzione per vengono inizializzate.

    Se non viene fornita una funzione di inizializzazione, RenderScript inizializza ogni accumulatore su zero (come se si trattasse di memset), comportandosi come se ci fosse un inizializzatore funzione simile alla seguente:

    static void initializerName(accumType *accum) {
      memset(accum, 0, sizeof(*accum));
    }
  • accumulator(accumulatorName) (obbligatorio): specifica il nome della funzione di accumulatore per questo di riduzione del kernel. All'avvio del kernel, RenderScript chiama questa funzione una volta per ogni coordinata negli input, per aggiornare dell'accumulatore in qualche modo in base agli input. La funzione devono essere definiti nel seguente modo:

    static void accumulatorName(accumType *accum,
                                in1Type in1, …, inNType inN
                                [, specialArguments]) { … }
    

    accum è un puntatore a un elemento dati accumulatore per questa funzione per modificare. Da in1 a inN sono uno o più argomenti che vengono compilati automaticamente in base agli input passati al lancio del kernel, un argomento per input. La funzione di accumulatore può facoltativamente accettare uno qualsiasi degli argomenti speciali.

    Un kernel di esempio con più input è dotProduct.

  • combiner(combinerName)

    (facoltativo): specifica il nome della funzione combinatore per questa di riduzione del kernel. Dopo che RenderScript chiama la funzione di accumulatore una volta per ogni coordinata negli input, chiama questa funzione volte quanto necessario per combinare tutti gli elementi di dati dell'accumulatore in un'unica accumulatore. La funzione deve essere definita come segue:

    static void combinerName(accumType *accum, const accumType *other) { … }

    accum è un puntatore a una "destinazione" accumulatore dati per da modificare. other è un puntatore a una "fonte" elemento dati accumulatore che questa funzione "combina" in *accum.

    NOTA:è possibile che *accum, *other o entrambi sono stati inizializzati ma non sono mai stati alla funzione di accumulatore; ovvero una o entrambe non sono mai state aggiornate in base ai dati di input. Ad esempio, nel il kernel findMinAndMax, il combinatore la funzione fMMCombiner verifica esplicitamente la presenza di idx < 0 perché indica un dato accumulatore il cui valore è INITVAL.

    Se non fornisci una funzione combinatore, RenderScript utilizza la funzione di accumulatore nel suo agendo come se ci fosse una funzione di combinatore simile alla seguente:

    static void combinerName(accumType *accum, const accumType *other) {
      accumulatorName(accum, *other);
    }

    Una funzione combinatore è obbligatoria se il kernel ha più di un input, se i dati di input non è uguale al tipo di dati accumulatore o se la funzione di accumulatore prende uno o più argomenti speciali.

  • outconverter(outconverterName) (facoltativo): specifica il nome della funzione di outconverter per questa di riduzione del kernel. Dopo che RenderScript ha combinato tutti gli accumulatori dati, chiama questa funzione per determinare il risultato per tornare a Java. La funzione deve essere definita come questo:

    static void outconverterName(resultType *result, const accumType *accum) { … }

    result è un puntatore a un elemento di dati dei risultati (allocato ma non inizializzato) dal runtime di RenderScript) affinché questa funzione venga inizializzata con il risultato e la riduzione del traffico. resultType è il tipo dell'elemento di dati, che non deve necessariamente essere lo stesso. come accumType. accum è un puntatore all'elemento dati dell'accumulatore finale calcolati dalla funzione combinata.

    Se non fornisci una funzione di outconverter, RenderScript copia l'accumulatore finale all'elemento di dati dei risultati, comportandosi come se ci fosse una funzione di outconverter che ha il seguente aspetto:

    static void outconverterName(accumType *result, const accumType *accum) {
      *result = *accum;
    }

    Se vuoi un tipo di risultato diverso dal tipo di dati accumulatore, la funzione di outconverter è obbligatoria.

Tieni presente che un kernel ha tipi di input, un tipo di elemento dati accumulatore e un tipo di risultato. nessuno dei quali deve essere uguale. Ad esempio, nel il kernel findMinAndMax, l'input tipo long, tipo di elemento dati accumulatore MinAndMax e risultato i tipi int2 sono tutti diversi.

Cosa non puoi dare per scontato?

Non devi fare affidamento sul numero di elementi dati accumulatori creati da RenderScript per un un dato lancio del kernel. Non c'è alcuna garanzia che due avvii dello stesso kernel con gli stessi input creano lo stesso numero di elementi di dati accumulatori.

Non devi fare affidamento sull'ordine in cui RenderScript chiama l'inizializzatore, l'accumulatore e funzioni di combinatore; potrebbe persino chiamarne alcuni in parallelo. Non vi è alcuna garanzia che due lanci dello stesso kernel con lo stesso input seguiranno lo stesso ordine. L'unico è che solo la funzione di inizializzazione vedrà mai un accumulatore non inizializzato dati. Ad esempio:

  • Non vi è alcuna garanzia che tutti gli elementi dati accumulatori vengano inizializzati prima del funzione di accumulatore viene chiamata, anche se verrà chiamata solo su un accumulatore inizializzato dati.
  • Non c'è alcuna garanzia sull'ordine in cui gli elementi di input vengono passati all'accumulatore personalizzata.
  • Non vi è alcuna garanzia che la funzione accumulatore sia stata chiamata per tutti gli elementi di input prima che venga chiamata la funzione combinatore.

Una conseguenza di ciò è che il valore findMinAndMax il kernel non è deterministico: se l'input contiene più di un'occorrenza della stessa valore minimo o massimo, non hai modo di sapere quale occorrenza da trovare.

Cosa devi garantire?

Poiché il sistema RenderScript può scegliere di eseguire un kernel in molti modi diversi, devi seguire alcune regole per assicurarti che il kernel funzioni come preferisci. Se non segui queste regole, potresti ricevere risultati errati, comportamento non deterministico o errori di runtime.

Le regole riportate di seguito spesso prevedono che due elementi dati accumulatori debbano avere "il stesso valore". Che cosa significa? Dipende da ciò che vuoi che faccia il kernel. Per una riduzione matematica come addint, di solito ha senso per "lo stesso" per indicare l'uguaglianza matematica. Per un "scegli qualsiasi" cerca tali come findMinAndMax ("trova le località di minimo e massimi di input") per cui potrebbe esserci più di un'occorrenza di input identici tutte le posizioni di un determinato valore di input devono essere considerate "uguali". Potresti scrivere un kernel simile per "trovare la posizione dei valori di input minimo e massimo a sinistra" dove (ad esempio) è preferibile un valore minimo nella località 100 rispetto a un valore minimo identico nella località 200; per questo kernel, "lo stesso" significherebbe una località identica, non solo valore identico e le funzioni di accumulatore e combinatore dovrebbero essere diversi da quelli di findMinAndMax.

La funzione di inizializzazione deve creare un valore di identità. Vale a dire che se I e A sono elementi di dati accumulatori inizializzati dalla funzione di inizializzazione e I non è mai stato passato funzione di accumulatore (ma A potrebbe averlo fatto), quindi
    .
  • combinerName(&A, &I) deve lascia A lo stesso
  • combinerName(&I, &A) deve lascia I lo stesso di A

Esempio: nel componente aggiuntivo un elemento dati accumulatore viene inizializzato a zero. La funzione di combinatore per questo il kernel esegue l'aggiunta; zero è il valore dell'identità da aggiungere.

Esempio: nella colonna findMinAndMax un elemento dati accumulatore viene inizializzato a INITVAL.

  • fMMCombiner(&A, &I) lascia A invariato, perché I è INITVAL.
  • fMMCombiner(&I, &A) imposta I a A, perché I è INITVAL.

Pertanto, INITVAL è effettivamente un valore dell'identità.

La funzione combinatore deve essere commutativa. Vale a dire che se A e B sono elementi di dati accumulatori inizializzati dalla funzione inizializzatore e che potrebbe essere stato passato alla funzione accumulatore zero o più volte, combinerName(&A, &B) deve imposta A sullo stesso valore che combinerName(&B, &A) imposta B.

Esempio: nel componente aggiuntivo kernel, la funzione combinatore somma i valori dei due dati relativi all'accumulo; l'addizione è commutativa.

Esempio: nel kernel findMinAndMax, fMMCombiner(&A, &B) è uguale a A = minmax(A, B) e minmax sono commutativi, quindi fMMCombiner.

La funzione combinatore deve essere associativa. Vale a dire che se A, B e C sono degli elementi di dati dell'accumulatore inizializzati dalla funzione di inizializzazione e che potrebbero essere stati passati alla funzione accumulatore zero o più volte, le seguenti due sequenze di codici devono Imposta A sullo stesso valore:

  • combinerName(&A, &B);
    combinerName(&A, &C);
    
  • combinerName(&B, &C);
    combinerName(&A, &B);
    

Esempio:nel kernel addint, la proprietà funzione di combinazione somma i due valori degli elementi di dati dell'accumulatore:

  • A = A + B
    A = A + C
    // Same as
    //   A = (A + B) + C
    
  • B = B + C
    A = A + B
    // Same as
    //   A = A + (B + C)
    //   B = B + C
    

L'addizione è associativa, quindi lo è anche la funzione combinatore.

Esempio: nel kernel findMinAndMax,

fMMCombiner(&A, &B)
è uguale a
A = minmax(A, B)
Quindi le due sequenze sono

  • A = minmax(A, B)
    A = minmax(A, C)
    // Same as
    //   A = minmax(minmax(A, B), C)
    
  • B = minmax(B, C)
    A = minmax(A, B)
    // Same as
    //   A = minmax(A, minmax(B, C))
    //   B = minmax(B, C)
    

minmax è associativo, così come lo è anche fMMCombiner.

La funzione di accumulatore e la funzione combinatore devono rispettare le norme di base regola pieghevole. Vale a dire, se A e B sono elementi di dati accumulatori, A è stato inizializzato dalla funzione di inizializzazione e possono essere stati passati alla funzione accumulatore zero o più volte, B non è stato inizializzato e args è l'elenco di argomenti di input e argomenti speciali per una particolare chiamata all'accumulatore , le due sequenze di codice seguenti devono impostare A con lo stesso valore:

  • accumulatorName(&A, args);  // statement 1
    
  • initializerName(&B);        // statement 2
    accumulatorName(&B, args);  // statement 3
    combinerName(&A, &B);       // statement 4
    

Esempio:nel kernel dell'addint, per un valore di input V:

  • L'istruzione 1 è uguale a A += V
  • L'istruzione 2 è uguale a B = 0
  • L'istruzione 3 è uguale a B += V, che equivale a B = V
  • L'istruzione 4 è uguale a A += B, che equivale a A += V

Le istruzioni 1 e 4 impostano A sullo stesso valore, quindi questo kernel obbedisce alla regola pieghevole di base.

Esempio:nel kernel findMinAndMax, per un input valore V alla coordinata X:

  • L'istruzione 1 è uguale a A = minmax(A, IndexedVal(V, X))
  • L'istruzione 2 è uguale a B = INITVAL
  • L'istruzione 3 è uguale a
    B = minmax(B, IndexedVal(V, X))
    
    che, poiché B è il valore iniziale, è uguale a
    B = IndexedVal(V, X)
    
  • L'istruzione 4 è uguale a
    A = minmax(A, B)
    
    che equivale a
    A = minmax(A, IndexedVal(V, X))
    

Le istruzioni 1 e 4 impostano A sullo stesso valore, quindi questo kernel obbedisce alla regola pieghevole di base.

Chiamata a un kernel di riduzione dal codice Java

Per un kernel di riduzione denominato kernelName definito nel filename.rs, ci sono tre metodi riportati nella classe ScriptC_filename:

Kotlin

// Function 1
fun reduce_kernelName(ain1: Allocation, …,
                               ainN: Allocation): javaFutureType

// Function 2
fun reduce_kernelName(ain1: Allocation, …,
                               ainN: Allocation,
                               sc: Script.LaunchOptions): javaFutureType

// Function 3
fun reduce_kernelName(in1: Array<devecSiIn1Type>, …,
                               inN: Array<devecSiInNType>): javaFutureType

Java

// Method 1
public javaFutureType reduce_kernelName(Allocation ain1, …,
                                        Allocation ainN);

// Method 2
public javaFutureType reduce_kernelName(Allocation ain1, …,
                                        Allocation ainN,
                                        Script.LaunchOptions sc);

// Method 3
public javaFutureType reduce_kernelName(devecSiIn1Type[] in1, …,
                                        devecSiInNType[] inN);

Ecco alcuni esempi di chiamata al kernel addint:

Kotlin

val script = ScriptC_example(renderScript)

// 1D array
//   and obtain answer immediately
val input1 = intArrayOf()
val sum1: Int = script.reduce_addint(input1).get()  // Method 3

// 2D allocation
//   and do some additional work before obtaining answer
val typeBuilder = Type.Builder(RS, Element.I32(RS)).apply {
    setX()
    setY()
}
val input2: Allocation = Allocation.createTyped(RS, typeBuilder.create()).also {
    populateSomehow(it) // fill in input Allocation with data
}
val result2: ScriptC_example.result_int = script.reduce_addint(input2)  // Method 1
doSomeAdditionalWork() // might run at same time as reduction
val sum2: Int = result2.get()

Java

ScriptC_example script = new ScriptC_example(renderScript);

// 1D array
//   and obtain answer immediately
int input1[] = ;
int sum1 = script.reduce_addint(input1).get();  // Method 3

// 2D allocation
//   and do some additional work before obtaining answer
Type.Builder typeBuilder =
  new Type.Builder(RS, Element.I32(RS));
typeBuilder.setX();
typeBuilder.setY();
Allocation input2 = createTyped(RS, typeBuilder.create());
populateSomehow(input2);  // fill in input Allocation with data
ScriptC_example.result_int result2 = script.reduce_addint(input2);  // Method 1
doSomeAdditionalWork(); // might run at same time as reduction
int sum2 = result2.get();

Il Metodo 1 ha un argomento Allocation di input per ogni argomento di input nell'accumulatore del kernel . Il runtime di RenderScript controlla che tutte le allocazioni di input abbiano le stesse dimensioni e che il tipo Element di ognuno le allocazioni di input corrispondono a quelle dell'argomento di input corrispondente dell'accumulatore il prototipo della funzione. Se uno di questi controlli non riesce, RenderScript genera un'eccezione. La il kernel viene eseguito su ogni coordinata in quelle dimensioni.

Il Metodo 2 è uguale al Metodo 1, ad eccezione del fatto che il Metodo 2 prende un'ulteriore l'argomento sc che può essere usato per limitare l'esecuzione del kernel a un sottoinsieme delle coordinate.

Il metodo 3 è uguale al metodo 1, ad eccezione del fatto che invece di prendere input di allocazione, prende input di array Java. Questa è una comodità evitando di scrivere codice per creare esplicitamente un'allocazione e copiarvi dati da un array Java. Tuttavia, l'utilizzo del Metodo 3 invece del Metodo 1 non aumenta le prestazioni del codice. Per ogni array di input, il Metodo 3 crea una l'allocazione unidimensionale con il tipo Element appropriato e setAutoPadding(boolean) abilitato e copia l'array nella Allocazione come se fosse il metodo copyFrom() appropriato di Allocation. Quindi chiama il Metodo 1, superando Allocazioni.

NOTA: se la tua applicazione effettua più chiamate kernel con lo stesso array o con array diversi delle stesse dimensioni e dello stesso tipo di elemento, potresti migliorare il rendimento creando, compilando e riutilizzando le allocazioni in modo esplicito, anziché utilizzando il metodo 3.

javaFutureType, il tipo restituito dei metodi di riduzione riflessa, classe nidificata statica all'interno di ScriptC_filename . Rappresenta il risultato futuro di una riduzione dell'esecuzione del kernel. Per ottenere il risultato effettivo dell'esecuzione, chiama il metodo get() di quella classe, che restituisce un valore di tipo javaResultType. get() è sincrona.

Kotlin

class ScriptC_filename(rs: RenderScript) : ScriptC(…) {
    object javaFutureType {
        fun get(): javaResultType { … }
    }
}

Java

public class ScriptC_filename extends ScriptC {
  public static class javaFutureType {
    public javaResultType get() { … }
  }
}

javaResultType viene determinato in base a resultType del funzione di outconverter. A meno che resultType non sia un senza firma (scalare, vettoriale o array), javaResultType è l'espressione di tipo Java. Se resultType è un tipo non firmato ed esiste un tipo firmato Java più grande, allora javaResultType è il tipo firmato Java più grande; altrimenti si tratta tipo Java corrispondente. Ad esempio:

  • Se resultType è int, int2 o int[15], il valore di javaResultType è int, Int2, o int[]. Tutti i valori di resultType possono essere rappresentati per javaResultType.
  • Se resultType è uint, uint2 o uint[15], allora javaResultType è long, Long2, o long[]. Tutti i valori di resultType possono essere rappresentati per javaResultType.
  • Se resultType è ulong, ulong2, o ulong[15], quindi javaResultType è long, Long2 o long[]. Esistono alcuni valori di resultType che non possono essere rappresentati da javaResultType.

javaFutureType è il tipo di risultato futuro corrispondente a resultType dell'outconverter .

  • Se resultType non è un tipo array, allora javaFutureType è result_resultType.
  • Se resultType è un array di lunghezza Count con membri di tipo memberType, il valore di javaFutureType è resultArrayCount_memberType.

Ad esempio:

Kotlin

class ScriptC_filename(rs: RenderScript) : ScriptC(…) {

    // for kernels with int result
    object result_int {
        fun get(): Int = …
    }

    // for kernels with int[10] result
    object resultArray10_int {
        fun get(): IntArray = …
    }

    // for kernels with int2 result
    //   note that the Kotlin type name "Int2" is not the same as the script type name "int2"
    object result_int2 {
        fun get(): Int2 = …
    }

    // for kernels with int2[10] result
    //   note that the Kotlin type name "Int2" is not the same as the script type name "int2"
    object resultArray10_int2 {
        fun get(): Array<Int2> = …
    }

    // for kernels with uint result
    //   note that the Kotlin type "long" is a wider signed type than the unsigned script type "uint"
    object result_uint {
        fun get(): Long = …
    }

    // for kernels with uint[10] result
    //   note that the Kotlin type "long" is a wider signed type than the unsigned script type "uint"
    object resultArray10_uint {
        fun get(): LongArray = …
    }

    // for kernels with uint2 result
    //   note that the Kotlin type "Long2" is a wider signed type than the unsigned script type "uint2"
    object result_uint2 {
        fun get(): Long2 = …
    }

    // for kernels with uint2[10] result
    //   note that the Kotlin type "Long2" is a wider signed type than the unsigned script type "uint2"
    object resultArray10_uint2 {
        fun get(): Array<Long2> = …
    }
}

Java

public class ScriptC_filename extends ScriptC {
  // for kernels with int result
  public static class result_int {
    public int get() { … }
  }

  // for kernels with int[10] result
  public static class resultArray10_int {
    public int[] get() { … }
  }

  // for kernels with int2 result
  //   note that the Java type name "Int2" is not the same as the script type name "int2"
  public static class result_int2 {
    public Int2 get() { … }
  }

  // for kernels with int2[10] result
  //   note that the Java type name "Int2" is not the same as the script type name "int2"
  public static class resultArray10_int2 {
    public Int2[] get() { … }
  }

  // for kernels with uint result
  //   note that the Java type "long" is a wider signed type than the unsigned script type "uint"
  public static class result_uint {
    public long get() { … }
  }

  // for kernels with uint[10] result
  //   note that the Java type "long" is a wider signed type than the unsigned script type "uint"
  public static class resultArray10_uint {
    public long[] get() { … }
  }

  // for kernels with uint2 result
  //   note that the Java type "Long2" is a wider signed type than the unsigned script type "uint2"
  public static class result_uint2 {
    public Long2 get() { … }
  }

  // for kernels with uint2[10] result
  //   note that the Java type "Long2" is a wider signed type than the unsigned script type "uint2"
  public static class resultArray10_uint2 {
    public Long2[] get() { … }
  }
}

Se javaResultType è un tipo di oggetto (incluso un tipo di array), ogni chiamata a javaFutureType.get() sulla stessa istanza restituirà lo stesso .

Se javaResultType non può rappresentare tutti i valori di tipo resultType e un il kernel di riduzione produce un valore non rappresentabile, javaFutureType.get() genera un'eccezione.

Metodo 3 e devecSiInXType

devecSiInXType è il tipo Java corrispondente a inXType dell'argomento corrispondente di la funzione accumulatore. A meno che inXType sia un senza firma o di tipo vettoriale, devecSiInXType è il tipo Java di testo. Se inXType è un tipo scalare non firmato, devecSiInXType è il valore Tipo Java corrispondente direttamente al tipo scalare con segno dello stesso dimensioni. Se inXType è un tipo vettoriale firmato, devecSiInXType è il valore Java. che corrisponde direttamente al tipo di componente vettoriale. Se inXType è un nome non firmato , devecSiInXType è il tipo Java che corrisponde direttamente al tipo scalare con segno delle stesse dimensioni del tipo di componente vettoriale. Ad esempio:

  • Se inXType è int, allora devecSiInXType è int.
  • Se inXType è int2, allora devecSiInXType è int. L'array è una rappresentazione lineare semplificata: ha il doppio Molti elementi scalatori poiché l'allocazione ha un vettore a due componenti. Elementi. È come funzionano i metodi copyFrom() di Allocation.
  • Se inXType è uint, allora deviceSiInXType è int. Un valore con segno nell'array Java viene interpretato come un valore non firmato di lo stesso bitpattern nell'allocazione. È lo stesso che copyFrom() di lavoro di Allocation.
  • Se inXType è uint2, allora deviceSiInXType è int. È una combinazione dei metodi int2 e uint sono gestiti: l'array è una rappresentazione bidimensionale, mentre i valori con segno dell'array Java sono interpretati come valori dell'elemento non firmato di RenderScript.

Tieni presente che per il Metodo 3, i tipi di input vengono gestiti in modo diverso. rispetto ai tipi di risultati:

  • L'input vettoriale di uno script è bidimensionale sul lato Java, mentre il risultato vettoriale di uno script non lo è.
  • L'input non firmato di uno script è rappresentato come un input con segno delle stesse dimensioni nella mentre il risultato non firmato di uno script è rappresentato come un tipo con segno ampliato (tranne nel caso di ulong).

Altri kernel di riduzione di esempio

#pragma rs reduce(dotProduct) \
  accumulator(dotProductAccum) combiner(dotProductSum)

// Note: No initializer function -- therefore,
// each accumulator data item is implicitly initialized to 0.0f.

static void dotProductAccum(float *accum, float in1, float in2) {
  *accum += in1*in2;
}

// combiner function
static void dotProductSum(float *accum, const float *val) {
  *accum += *val;
}
// Find a zero Element in a 2D allocation; return (-1, -1) if none
#pragma rs reduce(fz2) \
  initializer(fz2Init) \
  accumulator(fz2Accum) combiner(fz2Combine)

static void fz2Init(int2 *accum) { accum->x = accum->y = -1; }

static void fz2Accum(int2 *accum,
                     int inVal,
                     int x /* special arg */,
                     int y /* special arg */) {
  if (inVal==0) {
    accum->x = x;
    accum->y = y;
  }
}

static void fz2Combine(int2 *accum, const int2 *accum2) {
  if (accum2->x >= 0) *accum = *accum2;
}
// Note that this kernel returns an array to Java
#pragma rs reduce(histogram) \
  accumulator(hsgAccum) combiner(hsgCombine)

#define BUCKETS 256
typedef uint32_t Histogram[BUCKETS];

// Note: No initializer function --
// therefore, each bucket is implicitly initialized to 0.

static void hsgAccum(Histogram *h, uchar in) { ++(*h)[in]; }

static void hsgCombine(Histogram *accum,
                       const Histogram *addend) {
  for (int i = 0; i < BUCKETS; ++i)
    (*accum)[i] += (*addend)[i];
}

// Determines the mode (most frequently occurring value), and returns
// the value and the frequency.
//
// If multiple values have the same highest frequency, returns the lowest
// of those values.
//
// Shares functions with the histogram reduction kernel.
#pragma rs reduce(mode) \
  accumulator(hsgAccum) combiner(hsgCombine) \
  outconverter(modeOutConvert)

static void modeOutConvert(int2 *result, const Histogram *h) {
  uint32_t mode = 0;
  for (int i = 1; i < BUCKETS; ++i)
    if ((*h)[i] > (*h)[mode]) mode = i;
  result->x = mode;
  result->y = (*h)[mode];
}

Altri esempi di codice

Il file BasicRenderScript, RenderScriptIntrinsic, e Hello Compute. esempi dimostrano ulteriormente l'uso delle API trattate in questa pagina.