Por que cudaMalloc () usa ponteiro para ponteiro?

Por exemplo, cudaMalloc((void**)&device_array, num_bytes);

Esta pergunta foi feita antes, e a resposta foi “porque cudaMalloc retorna um código de erro”, mas não entendi – o que um ponteiro duplo tem a ver com retornar um código de erro? Por que um ponteiro simples não pode fazer o trabalho?

Se eu escrever

 cudaError_t catch_status; catch_status = cudaMalloc((void**)&device_array, num_bytes); 

o código de erro será colocado em catch_status , e retornar um ponteiro simples para a memory GPU alocada deve ser suficiente, não é?

Em C, os dados podem ser passados ​​para funções por valor ou via passagem simulada por referência (isto é, por um ponteiro para os dados). Por valor é uma metodologia unidirecional, por ponteiro permite o stream de dados bidirecional entre a function e seu ambiente de chamada.

Quando um item de dados é passado para uma function através da lista de parâmetros da function, e espera-se que a function modifique o item de dados original para que o valor modificado apareça no ambiente de chamada, o método C correto para isso é passar o item de dados por ponteiro. Em C, quando passamos pelo ponteiro, pegamos o endereço do item a ser modificado, criando um ponteiro (talvez um ponteiro para um ponteiro neste caso) e entregamos o endereço para a function. Isso permite que a function modifique o item original (por meio do ponteiro) no ambiente de chamada.

Normalmente malloc retorna um ponteiro, e podemos usar a atribuição no ambiente de chamada para atribuir esse valor retornado ao ponteiro desejado. No caso do cudaMalloc , os designers do CUDA escolheram usar o valor retornado para carregar um status de erro em vez de um ponteiro. Portanto, a configuração do ponteiro no ambiente de chamada deve ocorrer por meio de um dos parâmetros passados ​​para a function, por referência (ou seja, por ponteiro). Como é um valor de ponteiro que queremos definir, devemos pegar o endereço do ponteiro (criando um ponteiro para um ponteiro) e passar esse endereço para a function cudaMalloc .

Adicionando a resposta de Robert, mas para primeiro reiterar, é uma API C, o que significa que não suporta referências, o que permitiria que você modifique o valor de um ponteiro (não apenas o que está apontado) dentro da function . A resposta de Robert Crovella explicou isso. Observe também que ele precisa ser void porque o C também não suporta a sobrecarga de function.

Além disso, ao usar uma API C dentro de um programa C ++ (mas você não declarou isso), é comum include essa function em um modelo. Por exemplo,

 template cudaError_t cudaAlloc(T*& d_p, size_t elements) { return cudaMalloc((void**)&d_p, elements * sizeof(T)); } 

Existem duas diferenças em como você chamaria a function cudaAlloc acima:

  1. Passe o ponteiro do dispositivo diretamente, sem usar o operador address-of ( & ) ao chamá-lo e sem transmitir para um tipo void .
  2. O segundo argumento é agora o número de elementos, e não o número de bytes. O operador sizeof facilita isso. Isto é sem dúvida mais intuitivo para especificar elementos e não se preocupar com bytes.

Por exemplo:

 float *d = nullptr; // floats, 4 bytes per elements size_t N = 100; // 100 elements cudaError_t err = cudaAlloc(d,N); // modifies d, input is not bytes if (err != cudaSuccess) std::cerr << "Unable to allocate device memory" << std::endl; 

Eu acho que a assinatura da function cudaMalloc poderia ser melhor explicada por um exemplo. Basicamente, é atribuído um buffer através de um ponteiro para esse buffer (um ponteiro para o ponteiro), como o seguinte método:

 int cudaMalloc(void **memory, size_t size) { int errorCode = 0; *memory = new char[size]; return errorCode; } 

Como você pode ver, o método usa um ponteiro de memory para o ponteiro, no qual ele salva a nova memory alocada. Em seguida, ele retorna o código de erro (neste caso, como um inteiro, mas na verdade é um enum).

A function cudaMalloc pode ser projetada da seguinte forma:

 void * cudaMalloc(size_t size, int * errorCode = nullptr) { if(errorCode) errorCode = 0; char *memory = new char[size]; return memory; } 

Nesse segundo caso, o código de erro é definido por meio de um conjunto implícito de ponteiro para nulo (para o caso de as pessoas não se incomodarem com o código de erro). Em seguida, a memory alocada é retornada.

O primeiro método pode ser usado como é o atual cudaMalloc agora:

 float *p; int errorCode; errorCode = cudaMalloc((void**)&p, sizeof(float)); 

Enquanto o segundo pode ser usado da seguinte forma:

 float *p; int errorCode; p = (float *) cudaMalloc(sizeof(float), &errorCode); 

Estes dois methods são funcionalmente equivalentes, enquanto eles têm assinaturas diferentes, e as pessoas de cuda decidiram ir para o primeiro método, retornando o código de erro e atribuindo a memory através de um ponteiro, enquanto a maioria das pessoas diz que o segundo método teria sido um melhor escolha.