¿Por qué cudaMalloc () usa el puntero al puntero?

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

Esta pregunta se ha hecho antes, y la respuesta fue “porque cudaMalloc devuelve un código de error”, pero no lo entiendo: ¿qué tiene que hacer un doble puntero al devolver un código de error? ¿Por qué un simple puntero no puede hacer el trabajo?

Si escribo

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

el código de error se colocará en catch_status , y devolver un puntero simple a la memoria GPU asignada debería ser suficiente, ¿no es así?

En C, los datos se pueden pasar a las funciones por valor o mediante referencia simulada paso a paso (es decir, mediante un puntero a los datos). Por valor es una metodología de una sola vía, por puntero permite el flujo de datos bidireccionales entre la función y su entorno de llamada.

Cuando un elemento de datos se pasa a una función a través de la lista de parámetros de función y se espera que la función modifique el elemento de datos original para que el valor modificado aparezca en el entorno de llamada, el método correcto de C para esto es pasar el elemento de datos por puntero. En C, cuando pasamos por el puntero, tomamos la dirección del elemento que se va a modificar, creando un puntero (tal vez un puntero a un puntero en este caso) y le entregamos la dirección a la función. Esto permite que la función modifique el elemento original (mediante el puntero) en el entorno de llamada.

Normalmente malloc devuelve un puntero, y podemos usar la asignación en el entorno de llamada para asignar este valor devuelto al puntero deseado. En el caso de cudaMalloc , los diseñadores de CUDA eligieron usar el valor devuelto para llevar un estado de error en lugar de un puntero. Por lo tanto, la configuración del puntero en el entorno de llamada debe producirse a través de uno de los parámetros pasados ​​a la función, por referencia (es decir, mediante un puntero). Dado que es un valor de puntero que queremos establecer, debemos tomar la dirección del puntero (creando un puntero a un puntero) y pasar esa dirección a la función cudaMalloc .

Añadiendo a la respuesta de Robert, pero primero para reiterar, es una API C, lo que significa que no admite referencias, lo que le permitiría modificar el valor de un puntero (no solo lo que apunta) dentro de la función . La respuesta de Robert Crovella lo explicó. También tenga en cuenta que debe ser void porque C tampoco admite la sobrecarga de funciones.

Además, cuando se utiliza una API C dentro de un progtwig C ++ (pero no se ha indicado esto), es común ajustar dicha función en una plantilla. Por ejemplo,

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

Hay dos diferencias con la forma en que llamaría a la función cudaAlloc anterior:

  1. Pase el puntero del dispositivo directamente, sin utilizar el operador de dirección ( & ) al llamarlo y sin convertirlo en un tipo void .
  2. Los segundos elements argumento ahora son la cantidad de elementos en lugar de la cantidad de bytes. El operador sizeof facilita. Esto es posiblemente más intuitivo para especificar elementos y no preocuparse por los bytes.

Por ejemplo:

 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; 

Supongo que la firma de la función cudaMalloc podría explicarse mejor con un ejemplo. Básicamente es asignar un búfer a través de un puntero a ese búfer (un puntero a puntero), como el siguiente método:

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

Como puede ver, el método lleva un puntero de memory a un puntero, en el que guarda la nueva memoria asignada. Luego devuelve el código de error (en este caso como un entero, pero en realidad es una enumeración).

La función cudaMalloc podría diseñarse como sigue también:

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

En este segundo caso, el código de error se establece a través de un puntero implícito establecido en nulo (para el caso, las personas no se molestan en absoluto con el código de error). Luego se devuelve la memoria asignada.

El primer método se puede usar como es el actual cudaMalloc :

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

Mientras que el segundo se puede usar de la siguiente manera:

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

Estos dos métodos son funcionalmente equivalentes, mientras que tienen diferentes firmas, y las personas de cuda decidieron ir por el primer método, devolver el código de error y asignar la memoria a través de un puntero, mientras que la mayoría de las personas dicen que el segundo método habría sido un mejor elección.