C: Возврат пустоты по сравнению с возвратом двойного * от подфункции

Я работаю над попыткой ускорить некоторую общую обработку данных в C. Я записал несколько подпрограмм формы:

double *do_something(double *arr_in, ...) {
   double *arr_out; 
   arr_out = malloc(...)

   for (...) {
     do the something on arr_in and put into arr_out
   }

   return arr_out; 
}

Мне нравится этот стиль, потому что легко считать и использовать, но часто я называю его как:

 array = do_something(array,...);

Был бы он делать для более быстрого кода (и, возможно, предотвратите утечки памяти), если я должен был вместо этого использовать пустые подфункции как:

void do_something(double *arr_in, ...) {
   for (...) {
      arr_in = do that something;
   }
   return;
}

обновление 1: Я выполнил valgrind - leak-check=full на исполняемом файле, и кажется, что не было никаких утечек памяти с помощью первого метода. Однако исполняемый файл связывается с библиотекой, которая содержит все подпрограммы, которые я сделал с этой формой, таким образом, это не могло бы поймать утечки из библиотеки.

Мне любопытно относительно того, как я записал бы обертки для освобождения памяти и что ** действительно делает, или каков указатель на указатель, таким образом, я избегаю использования ** маршрут (это, и возможно я сделал это неправильно, потому что это не скомпилировало на моем Mac).

Вот одна текущая подпрограмма:

double *cos_taper(double *arr_in, int npts)
{
int i;
double *arr_out;
double cos_taper[npts];
int M; 
M = floor( ((npts - 2) / 10) + .5);

arr_out = malloc(npts*sizeof(arr_out));

for (i=0; i<npts; i++) {
    if (i<M) {
        cos_taper[i] = .5 * (1-cos(i * PI / (M + 1)));
    }
    else if (i<npts - M - 2) {
        cos_taper[i] = 1;
    }
    else if (i<npts) {
        cos_taper[i] = .5 * (1-cos((npts - i - 1) * PI / (M + 1)));
    }
    arr_out[i] = arr_in[i] * cos_taper[i];
}
return arr_out;
}

От совета я добрался здесь, он кажется, что лучший метод был бы:

void *cos_taper(double *arr_in, double *arr_out, int npts)
{
int i;
double cos_taper[npts];
int M; 
M = floor( ((npts - 2) / 10) + .5);

for (i=0; i<npts; i++) {
    if (i<M) {
        cos_taper[i] = .5 * (1-cos(i * PI / (M + 1)));
    }
    else if (i<npts - M - 2) {
        cos_taper[i] = 1;
    }
    else if (i<npts) {
        cos_taper[i] = .5 * (1-cos((npts - i - 1) * PI / (M + 1)));
    }
    arr_out[i] = arr_in[i] * cos_taper[i];
}
return
}

вызов:

int main() {
  int npts;
  double *data, *cos_tapered;

  data = malloc(sizeof(data) * npts);
  cos_tapered = malloc(sizeof(cos_tapered) * npts);

//fill data

  cos_taper(data, cos_tapered, npts);
  free(data);
  ...
  free(cos_tapered);
  ...
  return 0;
}
6
задан Rob Porritt 5 February 2010 в 19:15
поделиться

12 ответов

Маллок может быть дорогим по отношению к выполняемой обработке, в зависимости от того, что это такое. Вместо того, чтобы ограничиваться обработкой на месте, просто используйте два параметра, входящий и исходящий, и оставьте распределение вызывающему абоненту. Это дает абоненту возможность повторно использовать память без выделения нового массива для каждого вызова.

5
ответ дан 10 December 2019 в 00:38
поделиться

Итак, вы начали свой вопрос, говоря о скорости, и я не думаю, что на эту тему действительно ответили. Первое, что нужно сказать, это то, что работа с передачей параметров, похоже, не лучший способ ускорить процесс ...

Я согласен с другими ответами: первое предложение с использованием malloc - это путь к утечкам памяти (и, вероятно, в любом случае медленнее ), другое предложение, которое вы придумали, намного лучше. Следуя предложениям ergosys в комментариях, вы можете легко улучшить его и получить хороший код C.

Теперь с помощью математики вы все еще можете поправиться.

Во-первых, нет необходимости использовать вызовы double и floor для вычисления целых чисел. Вы получаете то же M без пола и не добавляете 0,5, просто записывая M = (nbelts-2) / 10; (Подсказка: целочисленное деление усекается до целого).

Если вы также заметили, что у вас всегда есть M

Ваша функция могла бы выглядеть примерно так:

void cos_taper(double *arr_in, double *arr_out, int npts)
{
int i;
int M; 
M = (npts - 2) / 10;

if (arr_out == arr_in) {
    for (i=0; i<M; i++) {
        arr_out[i] *= .5 * (1-cos(i * PI / (M + 1)));
    }
    for (i = npts - M - 2; i<npts; i++) {
        arr_out[i] *= .5 * (1-cos((npts - i - 1) * PI / (M + 1)));
    }
}
else {
    for (i=0; i<M; i++) {
        arr_out[i] = arr_in[i] * (.5 * (1-cos(i * PI / (M + 1))));
    }
    for (; i<npts - M - 2; i++) {
        arr_out[i] = arr_in[i];
    }
    for (; i<npts; i++) {
        arr_out[i] = arr_in[i] * (.5 * (1-cos((npts - i - 1) * PI / (M + 1))));
    }
}
}

Это определенно не конец, и если подумать, возможны дополнительные оптимизации, например такие выражения, как (. 5 * (1-cos (i * PI / (M + 1)))); похоже, что они могут получить относительно небольшое количество значений (зависит от размера nbelt, поскольку это функция от i и nbelt, количество различных результатов является квадратичным законом, но cos следует снова уменьшить это число, так как оно периодическое). Но все зависит от того, какой уровень производительности вам нужен.

0
ответ дан 10 December 2019 в 00:38
поделиться

Тип функции определяет интерфейс между функцией и местами в вызывающем ее коде, то есть, скорее всего, при выборе будут иметь место важные проблемы проектирования кода. Как правило, о них стоит задуматься, а не о скорости (при условии, что проблема скорости не в утечке памяти настолько велика, что приложение страдает от DOS через "мусорку"...)

Второй тип в основном указывает на то, что массив мутирует. Первый неоднозначен: может быть, вы всегда будете мутировать массив, может быть, вы всегда предоставите свежий результат выделения, может быть, ваш код иногда делает одно, а иногда - другое. Свобода сопровождается минным полем трудностей, связанных с проверкой корректности кода. Если вы пойдёте этим путём, то попытка либерального разбрызгивания assert()s по вашему коду, для утверждения инвариантов свежести и разделения указателей, скорее всего, окупится с достаточным процентом при отладке.

0
ответ дан 10 December 2019 в 00:38
поделиться

У вас также есть возможность передать второй параметр в качестве выходного параметра. Например

int do_something (double * in , double * out) {
   /*
    * Do stuff here
    */
   if (out is modified)
      return 1;
   return 0;
}

Или аналогичный без возврата.

0
ответ дан 10 December 2019 в 00:38
поделиться

Я бы посоветовал, если вы выделяете память внутри подфункции, вы либо создаете соответствующую оболочку для очистки, либо освобождаете выделенную память, либо делаете ослепляюще очевидным, что функция выделяет память, чтобы не забыть освободить память.

Что касается объема памяти, то при втором подходе будет использоваться меньше памяти, но он работает только в том случае, если функции не изменяют размер исходного массива. В зависимости от использования это не всегда так.

Что касается скорости, второй подход теоретически должен быть быстрее, потому что в конце вызова функции в стек помещается на один указатель меньше ( do_something ), но единственный указатель имеет минимальную разницу, если только существует частое повторное использование, и в этом случае следует тщательно продумать встраивание. Поэтому, если вы на самом деле не измерили накладные расходы на вызов функции как проблему (путем профилирования ), я бы не стал возиться с такими микрооптимизациями без уважительной причины (объем памяти или профилирование).

0
ответ дан 10 December 2019 в 00:38
поделиться

Первый вызов может легко вызвать утечку памяти, если нет другого указателя на исходное выделение памяти - как вы, вероятно, знаете, поскольку спрашиваете.

Да, если вы можете разумно написать вторую версию вызываемой функции без выделения памяти, она, вероятно, будет быстрее, потому что выделение памяти требует времени. Если вы просто измените вызываемую функцию, чтобы она имела предварительно выделенные входные и выходные массивы, она может просто передать стоимость выделения памяти вызывающей функции.

Но дисциплинированное использование первой версии - это нормально; функция выделяет пространство, и пока вы отслеживаете как исходное переданное пространство, так и новое, переданное обратно, и можете освободить и то, и другое, проблем нет.

Вы можете столкнуться с «той же» проблемой с помощью:

xyz = realloc(xyz, newsize);

Если xyz является единственным указателем на выделенную память, это приводит к утечке памяти при сбое выделения, потому что вы только что заблокировали xyz нулевым указателем. Если есть другой указатель, который вы будете использовать для освобождения исходного пространства, эта идиома не имеет значения, но будьте осторожны с ней.


Я не полностью усвоил дополнительную информацию в вопросе с момента написания исходной версии этого ответа.

1
ответ дан 10 December 2019 в 00:38
поделиться

Я бы предпочел позволить вызывающему абоненту выделять память, если он хочет, но также имел бы возможность выбрать выполнение операции на месте или вы делаете распределение.

Для операций, которые невозможно выполнить на месте, вы можете вручную проверить, предоставил ли вызывающий абонент одинаковые адреса ввода и вывода, и сделать копию ввода самостоятельно. Затем обработайте, используя эту копию в качестве входных данных. Это заставляет его выглядеть на месте для вызывающей функции.

Например, предположим, что вы хотите создать функцию, которая выполняет перемешивание массива индексов так, чтобы output [i] == input [input [i]] (глупая функция, правда, но тот, который нетривиально сделать на месте).

#include <stdlib.h> 
#include <string.h>
int shuffle(size_t const * input, size_t const size, size_t ** p_output)
{
    int retval = 0;
    size_t i;
    char in_place = 0;
    char cleanup_output = 0;

    if (size == 0)
    {
        return 0; // nothing to do
    }
    // make sure we can read our input and write our output
    else if (input == NULL || p_output == NULL)
    {
        return -2; // illegal input
    }
    // allocate memory for the output
    else if (*p_output == NULL)
    {
        *p_output = malloc(size * sizeof(size_t));
        if (*p_output == NULL) return -1; // memory allocation problem
        cleanup_output = 1; // free this memory if we run into errors
    }
    // use a copy of our input, since the algorithm doesn't operate in place.
    // and the input and output overlap at least partially
    else if (*p_output - size < input && input < *p_output + size)
    {
        size_t * const input_copy = malloc(size * sizeof(size_t));
        if (input_copy == NULL) return -1; // memory allocation problem
        memcpy( input_copy, input, size * sizeof(size_t));
        input = input_copy;
        in_place = 1;
    }

    // shuffle
    for (i = 0; i < size; i++)
    {
        if (input[i] >= size)
        {
            retval = -2; // illegal input
            break;
        }
        (*p_output)[i] = input[ input[i] ];
    }

    // cleanup
    if (in_place)
    {
         free((size_t *) input);
    }
    if (retval != 0 && cleanup_output)
    {
         free(*p_output);
         *p_output = NULL;
    }

    return retval;
}

Это делает вашу функцию более надежной - вызывающая функция может выделять память для вывода (если они хотят сохранить ввод), или выводить вывод в том же месте, что и ввод, или вы можете выделить память для выход. Это особенно приятно, если они сами получили адреса ввода и вывода откуда-то еще и не уверены, различны ли они. Им не нужно ничего знать о работе функции.

// caller allocated memory
my_allocated_mem = malloc( my_array_size * sizeof(size_t) );
if(my_allocated_mem == NULL) { /*... */ }
shuffle( my_array, my_array_size, &my_allocated_mem );

// function allocated memory
my_allocated_mem = NULL;
shuffle( my_array, my_array_size, &my_allocated_mem );

// in place calculation
shuffle( my_array, my_array_size, &my_array);

// (naughty user isn't checking the function for error values, but you get the idea...)

Вы можете увидеть полный пример использования здесь .

Поскольку C не имеет исключений, довольно стандартно использовать возвращаемое значение функции для сообщения об ошибках и передавать вычисленные значения обратно через указатель функции.

1
ответ дан 10 December 2019 в 00:38
поделиться

Если вы сможете выполнить свою операцию на месте, это, вероятно, поможет предотвратить ошибки (по крайней мере, связанные с памятью) и будет быстрее, по крайней мере, время, необходимое для выполнения операции malloc () . Фактический тип возврата вашей функции, вероятно, никак не влияет на скорость.

1
ответ дан 10 December 2019 в 00:38
поделиться

Вы сэкономите немного времени, не имея malloc, но это может быстро накапливаться и иметь заметную разницу, если вы вызываете do_something в тесном цикле. Вы также сэкономите немного времени, не возвращая двойное *, но опять же, это может складываться, если do_something вызывается часто.

Что касается самой обработки, не будет никакой разницы, поскольку оба случая работают с двойным *

Поскольку вы не используете динамическое распределение памяти в предложенном вами методе, вероятность утечки памяти больше не существует.

0
ответ дан 10 December 2019 в 00:38
поделиться

В вашей функции

void do_something(double *arr_in, ...) {
   for (...) {
      arr_in = do_that_something;
   }
}

это было бы неверно, поскольку у вас нет параметра по ссылке для передачи обратно массива после do_something функция выходит за рамки ... она должна выглядеть примерно так

void do_something(double **arr_in, ...) {
   for (...) {
      *arr_in = do_that_something;
   }
}
/*
** Would be called like this:
** do_something(&array, ...);
*/

Придерживайтесь первого примера, так как его легче читать. Вам нужно добавить проверку ошибок в первом примере, если вызов malloc завершился неудачно, и продолжить обработку с указателем NULL ...

Надеюсь, это поможет, С уважением, {{1 }}Том.

0
ответ дан 10 December 2019 в 00:38
поделиться

Возврат самого двойного значения не требует больших затрат времени выполнения.

Гораздо более важным является выделение памяти каждый раз, когда вы входите в функцию. Если вы можете предварительно выделить или сохранить результат на месте, как вы предложили, это должно значительно повысить скорость.

Еще одна вещь, которую следует учитывать, - нужна ли вам на самом деле вся точность, которую обеспечивает double (по сравнению с типом float). Во многих случаях поплавки работают намного быстрее.

1
ответ дан 10 December 2019 в 00:38
поделиться

Я только что запустил ваш код (после исправления ряда мелких ошибок). Затем я сделал несколько стэкшотов. Люди, которые говорили, что malloc будет вашим виновником, были правы. Почти все ваше время проводится там. По сравнению с этим, остальная часть вашего кода не очень значительна. Вот код:

#include <math.h>
#include <stdlib.h>
const double PI = 3.1415926535897932384626433832795;

void cos_taper(double *arr_in, double *arr_out, int npts){ 
    int i; 
//  double taper[npts];
    double* taper = (double*)malloc(sizeof(double) * npts); 
    int M;  
    M = (int)floor( ((npts - 2) / 10) + .5); 

    for (i=0; i<npts; i++){ 
        if (i<M) { 
            taper[i] = .5 * (1-cos(i * PI / (M + 1))); 
        } 
        else if (i<npts - M - 2) { 
            taper[i] = 1; 
        } 
        else if (i<npts) { 
            taper[i] = .5 * (1-cos((npts - i - 1) * PI / (M + 1))); 
        } 
        arr_out[i] = arr_in[i] * taper[i]; 
    }
    free(taper);
    return;
}

void doit(){
    int i;
    int npts = 100; 
    double *data, *cos_tapered; 

    data = (double*)malloc(sizeof(double) * npts); 
    cos_tapered = (double*)malloc(sizeof(double) * npts); 

    //fill data 
    for (i = 0; i < npts; i++) data[i] = 1;

    cos_taper(data, cos_tapered, npts); 
    free(data); 
    free(cos_tapered); 
}

int main(int argc, char* argv[]){
    int i;
    for (i = 0; i < 1000000; i++){
        doit();
    }
    return 0;
}

EDIT: Я засек время вышеприведенного кода, который занял 22us на моей машине (в основном в malloc). Затем я изменил его так, чтобы он выполнял malloc только один раз на внешней стороне. Это уменьшило время до 5.0us, что было в основном в функции cos. Затем я переключился с Debug на Release билд, что снизило время до 3.7us (теперь еще больше в функции cos, очевидно). Так что если вы действительно хотите сделать это быстро, я рекомендую стекшоты, чтобы выяснить, что вы в основном делаете, а затем посмотреть, можете ли вы избежать этого.

1
ответ дан 10 December 2019 в 00:38
поделиться
Другие вопросы по тегам:

Похожие вопросы: