Могу ли я оптимизировать этот кусок кода? - PullRequest
2 голосов
/ 29 октября 2009

Можно ли здесь использовать любую технику оптимизации цикла, чтобы сократить время выполнения? Мне нужен вложенный цикл с i и j, так как мне нужны эти комбинации (i, j).

РЕДАКТИРОВАТЬ: даже если я оставлю «фактический» код, с этим тривиальным назначением, это займет ~ 5 с на моем двухъядерном блоке, тогда как с этим фактическим кодом это займет ~ 6 с. Я экспериментировал с заменой fn_val + = 0 на j + = 0, и это занимает ~ 1,73 с. С чем это может быть связано?

# include <stdio.h>
# include <time.h>

int main(int argc, char **argv)
{
        float fn_value=0.0;
        int n=10,i,j;
        unsigned int k;
        clock_t start, end;

        start = clock();
        for(k=0;k<9765625;k++)
        {

                for(i=0;i<n;i++)
                {
                        for(j=i;j<n;j++)
// substitute for an "actual" piece of code
                                fn_value+=0; 
                }
        }
        end= clock();

        printf("Time taken %lf", (double) (end-start) / CLOCKS_PER_SEC);
        return 0;
}

Ответы [ 9 ]

2 голосов
/ 29 октября 2009

Возможно, вы захотите проверить OpenMP , так как вы можете запустить doStuff в разных потоках для разных индексов i, j и k, если doStuff поточно-ориентирован.

1 голос
/ 30 октября 2009

Поскольку ваш самый внутренний цикл имеет всего 10 итераций, он немного улучшит вашу скорость, если вы сможете объединить два внутренних цикла (всего 100 итераций).

1 голос
/ 29 октября 2009

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

Сначала я бы сосредоточился на «реальном» фрагменте кода. Есть ли какие-нибудь смарты, которые вы можете использовать, чтобы "заблокировать" вычисления там? Повторно используйте предыдущий ответ, чтобы дешево рассчитать следующий и т. Д.

1 голос
/ 29 октября 2009

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

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

Кроме того, вы компилируете с оптимизацией? (т.е. -O3 в гкц)

По вашему редактированию:

Причина, по которой "j + = 0" быстрее, чем "fn_val + = 0", заключается в том, что целочисленная арифметика НАМНОГО быстрее, чем операции с плавающей запятой.

Именно поэтому нам нужен фактический код, чтобы дать вам информированные оптимизации.

1 голос
/ 29 октября 2009

Ну, он вполне может работать параллельно, наверное.

0 голосов
/ 29 октября 2009

Вы используете код с плавающей запятой! Компиляторы - мусор с кодом с плавающей запятой.

Вот некоторые измерения, которые я сделал, я использую DevStudio 2005 с оптимизацией по умолчанию и немного изменил код:

// added to the inner part of the loop
fn_value += j; 

// added a dependancy on fn_value so that the compiler doesn't optimise the 
// whole code down to nothing
printf("Time taken %lf - %f", (double) (end-start) / CLOCKS_PER_SEC, fn_value);

Итак, я запустил это примерно через 5 секунд.

Теперь я немного изменил код:

# include <stdio.h>
# include <time.h>

int main(int argc, char **argv)
{
  int fn_value=0;
  int n=10,i,j;
  unsigned int k;
  clock_t start, end;

  start = clock();
  for(k=0;k<9765625;k++)
  {
    for(i=0;i<n;i++)
    {
      for(j=i;j<n;j++)
        fn_value+=j; 
    }
  }
  end= clock();

  printf("Time taken %lf - %d", (double) (end-start) / CLOCKS_PER_SEC, fn_value);
  return 0;
}

Я изменил значение fn_value на int. Теперь это занимает около секунды! Таким образом, между добавлением целых и добавлением чисел с плавающей запятой уходит четыре секунды Затем я написал версию с кодами операций IA32 FPU вместо кода C и получил примерно 1,4 секунды, что не намного медленнее, чем использование целых чисел.

Затем я использовал версию с плавающей запятой C, но сделал fn_value удвоенным, и время стало 1,25 с. Это меня удивило. Он превзошел версию кода операции FPU, но, глядя на разборку, единственное отличие заключается в том, что версия на чистом C развернула внутренний цикл.

Кроме того, при использовании чисел с плавающей точкой результат является неверным.

Вот мой последний тестовый код:

# include <stdio.h>
# include <time.h>

void p1 ()
{
  double fn_value=0;//if this is a float, the answer is slightly wrong
  int n=10,i,j;
  unsigned int k;
  clock_t start, end;

  start = clock();
  __asm fldz;
  for(k=0;k<9765625;k++)
  {
    for(i=0;i<n;i++)
    {
      for(j=i;j<n;j++)
        __asm {
          fiadd j
      }
    }
  }
  __asm fstp fn_value;
  end= clock();

  printf("p1: Time taken %lf - %lf\n", (double) (end-start) / CLOCKS_PER_SEC, (double) fn_value);
}

void p2 ()
{
  double fn_value=0;
  int n=10,i,j;
  unsigned int k;
  clock_t start, end;

  start = clock();
  for(k=0;k<9765625;k++)
  {
    for(i=0;i<n;i++)
    {
      for(j=i;j<n;j++)
        fn_value+=j; 
    }
  }
  end= clock();

  printf("p2: Time taken %lf - %lf\n", (double) (end-start) / CLOCKS_PER_SEC, (double) fn_value);
}

void p3 ()
{
  float fn_value=0;
  int n=10,i,j;
  unsigned int k;
  clock_t start, end;

  start = clock();
  for(k=0;k<9765625;k++)
  {
    for(i=0;i<n;i++)
    {
      for(j=i;j<n;j++)
        fn_value+=j; 
    }
  }
  end= clock();

  printf("p3: Time taken %lf - %lf\n", (double) (end-start) / CLOCKS_PER_SEC, (double) fn_value);
}

int main(int argc, char **argv)
{
  p1 ();
  p2 ();
  p3 ();
  return 0;
}

Таким образом, double кажется быстрее, чем float. Однако нам нужно увидеть содержимое этого внутреннего цикла, чтобы увидеть, даст ли преобразование типа с плавающей запятой какое-либо ускорение в вашем конкретном случае.

UPDATE

Причина, по которой версия с плавающей запятой медленнее, чем у других, заключается в том, что версия с плавающей запятой постоянно записывает и читает значение в / из памяти. Двойная и рукописная версии никогда не записывают значение в ОЗУ. Почему это так? Основная причина, по которой я могу придумать, состоит в том, чтобы снизить точность значения fn_value между операциями. Внутренне, FPU - 80 бит, тогда как число с плавающей запятой - 32 бита (в этой реализации C). Чтобы сохранить значения в диапазоне с плавающей запятой, компилятор преобразует из 80-битной в 32-битную запись и чтение значения в / из ОЗУ, потому что, насколько я знаю, нет инструкции FPU, чтобы сделать это для одного регистра FPU , Таким образом, для того, чтобы математика оставалась «32-битной» (типа float), она вносит огромные накладные расходы. Компилятор игнорирует различия между 80-битным FPU и 64-битным двойным типом и предполагает, что программист хочет максимально больший тип.

0 голосов
/ 29 октября 2009

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

Итак: -

for(i=0;i<n;i++)
{
    for(j=i;j<n;j++)
// substitute for an "actual" piece of code
        fn_value+=0; 
}

Становится (я думаю, всегда ошибаюсь арифметика;)): -

for(i=n;i--;)
{
    for(j=n-i;j--;)
// substitute for an "actual" piece of code
        fn_value+=0; 
}

Конечно, тогда ваши петли назад ...

Я бы хотел услышать, если это изменит! Мой инстинкт инстинкта заключается в том, что вы оптимизируете не то.

Ага, ссылка: - http://www.abarnett.demon.co.uk/tutorial.html#FASTFOR

0 голосов
/ 29 октября 2009

Это действительно зависит от того, что делает «заменитель реального фрагмента кода» и как значения i, j и k используются этим кодом. Если все i, j и k используются, то, на самом деле, вы можете сделать не так уж много (кроме многопоточности, хотя при использовании в математическом уравнении вы можете использовать некоторую умную алгебру, чтобы уменьшить сложность / повторение). расчетов). С другой стороны, если ни одно из значений не используется, то вы можете просто сделать его одним циклом, который будет выполняться указанное количество раз (хотя результаты могут отличаться в зависимости от компиляторов / уровней оптимизации).

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

fn_value = 0;
k = 9765625;
n = 10;
i = 10;
j = 10;

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

0 голосов
/ 29 октября 2009

Сами циклы, вероятно, не имеют значения, все зависит от того, сколько работы вы выполняете в самом внутреннем цикле.

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...