2013-12-09 1 views
5

Я выполнил бенчмаркинг, чтобы сравнить удвоение и производительность поплавка. Я был очень удивлен, увидев, что удваивается намного быстрее, чем плавает.Возможно ли, что удваивается x2 FASTER, чем float?

Я видел некоторые дискуссии о том, что, например:

Is using double faster than float?

Are doubles faster than floats in c#?

Большинство из них сказал, что это возможно, что двойные и плывут производительность будет похоже, из-за двойной точности оптимизация и т. д. Но я видел улучшение производительности x2 при использовании двухместных !! Как это возможно? Хуже всего то, что я использую 32-битную машину, которая, как ожидается, будет лучше работать для поплавков в соответствии с некоторыми сообщениями ...

Я использовал C# для проверки, но я вижу, что аналогичная реализация на C++ имеет аналогичное поведение.

код я, чтобы проверить его:

static void Main(string[] args) 
{ 
    double[,] doubles = new double[64, 64]; 
    float[,] floats = new float[64, 64]; 

    System.Diagnostics.Stopwatch s = new System.Diagnostics.Stopwatch(); 

    s.Restart(); 
    CalcDoubles(doubles); 
    s.Stop(); 
    long doubleTime = s.ElapsedMilliseconds; 

    s.Restart(); 
    CalcFloats(floats); 
    s.Stop(); 
    long floatTime = s.ElapsedMilliseconds; 

    Console.WriteLine("Doubles time: " + doubleTime + " ms"); 
    Console.WriteLine("Floats time: " + floatTime + " ms"); 
} 

private static void CalcDoubles(double[,] arr) 
{ 
    unsafe 
    { 
    fixed (double* p = arr) 
    { 
     for (int b = 0; b < 192 * 12; ++b) 
     { 
     for (int i = 0; i < 64; ++i) 
     { 
      for (int j = 0; j < 64; ++j) 
      { 
      double* addr = (p + i * 64 + j); 
      double arrij = *addr; 
      arrij = arrij == 0 ? 1.0f/(i * j) : arrij * (double)i/j; 
      *addr = arrij; 
      } 
     } 
     } 
    } 
    } 
} 

private static void CalcFloats(float[,] arr) 
{ 
    unsafe 
    { 
    fixed (float* p = arr) 
    { 
     for (int b = 0; b < 192 * 12; ++b) 
     { 
     for (int i = 0; i < 64; ++i) 
     { 
      for (int j = 0; j < 64; ++j) 
      { 
      float* addr = (p + i * 64 + j); 
      float arrij = *addr; 
      arrij = arrij == 0 ? 1.0f/(i * j) : arrij * (float)i/j; 
      *addr = arrij; 
      } 
     } 
     } 
    } 
    } 
} 

Я использую очень слабый ноутбук: процессор Intel Atom N455 (двухъядерный, 1.67GHz, 32bit) с 2 Гб оперативной памяти.

+0

Спасибо за ответ. Обратите внимание, что счетчик увеличивается в одном и том же смещении как для двоек, так и для поплавков (он вычисляется вручную в цикле for). – MaMazav

+1

Вам не нужен небезопасный код, чтобы воспроизвести это. Эффект исчезает, если вы удалите назначение обратно в массив. – Zong

+0

Вы правы, это происходит и на моей машине. И почему так? Может быть, политика кэширования обратной записи/написания (как, возможно, ранее говорилось)? – MaMazav

ответ

3

С # спецификации C:

операции с плавающей точкой может быть выполнена с более высокой точностью, чем тип результата операции. Например, некоторые аппаратные архитектуры поддерживают «расширенный» или «длинный двойной» с плавающей точкой тип с большим диапазоном и точностью, чем двойной тип, а неявно выполняют все операции с плавающей точкой, используя этот более высокий тип точности . Только при чрезмерной стоимости в производительности могут быть созданы такие аппаратные архитектуры для выполнения операций с плавающей запятой с меньшей точностью и вместо того, чтобы требовать реализации до , утрачивают как производительность, так и точность, C# позволяет использовать более высокую точность , которая будет использоваться для всех с плавающей запятой. Помимо , обеспечивая более точные результаты, это редко имеет измеримые эффекты . Однако в выражениях вида x * y/z, где умножение дает результат, выходящий за пределы двойного диапазона, но последующее деление возвращает временный результат в двойной диапазон , тот факт, что выражение оценивается в более высоком формате может привести к получению конечного результата вместо бесконечности .

Для его хранения в массиве могут потребоваться дополнительные инструкции для преобразования значения в 32-разрядный поплавок.

Кроме того, как упоминалось в документе accepted answer, к одному из вопросов, на которые вы ссылаетесь, спецификация CLI требует, чтобы в некоторых других случаях усекались 64-разрядные (или 80-битные) значения. Этот ответ также ссылки на дополнительное обсуждение здесь:

http://weblog.ikvm.net/PermaLink.aspx?guid=f300c4e1-15b0-45ed-b6a6-b5dc8fb8089e

10

Это выглядит джиттер оптимизатор роняет мяч здесь, он не подавляет избыточный магазин в случае с плавающей точкой.Горячий код является 1.0f/(i * j) расчет, так как все значения массива равны 0. x86 джиттера генерирует:

01062928 mov   eax,edx      ; eax = i 
0106292A imul  eax,esi      ; eax = i * j 
0106292D mov   dword ptr [ebp-10h],eax  ; store to mem 
01062930 fild  dword ptr [ebp-10h]   ; convert to double 
01062933 fstp  dword ptr [ebp-10h]   ; redundant store, convert to float 
01062936 fld   dword ptr [ebp-10h]   ; redundant load 
01062939 fld1         ; 1.0f 
0106293B fdivrp  st(1),st     ; 1.0f/(i * j) 
0106293D fstp  dword ptr [ecx]    ; arrij = result 

x64 джиттера:

00007FFCFD6440B0 cvtsi2ss xmm0,r10d   ; (float)(i * j) 
00007FFCFD6440B5 movss  xmm1,dword ptr [7FFCFD644118h] ; 1.0f 
00007FFCFD6440BD divss  xmm1,xmm0   ; 1.0f/(i * j) 
00007FFCFD6440C1 cvtss2sd xmm0,xmm1   ; redundant store 
00007FFCFD6440C5 cvtsd2ss xmm0,xmm0   ; redundant load 
00007FFCFD6440C9 movss  dword ptr [rax+r11],xmm0 ; arrij = result 

Я пометил лишние инструкции с «лишними». Оптимизатору удалось устранить их в версии double, чтобы код работал быстрее.

Резервные магазины на самом деле присутствуют в IL, сгенерированном компилятором C#, это задача оптимизатора для обнаружения и удаления. Примечательно, что джиттер x86 и x64 имеет этот недостаток, поэтому он выглядит как общий надзор в алгоритме оптимизатора.

Код x64 особенно примечателен тем, что для преобразования результата поплавка необходимо удвоить, а затем снова вернуться к поплавке, что указывает на то, что основная проблема - это преобразование типа данных, которое оно не знает, как подавить. Вы также видите это в коде x86, избыточное хранилище фактически делает преобразование с двойным преобразованием. Устранить преобразование в случае x86 сложно, так что это вполне может просочиться в джиттер x64.

Обратите внимание, что код x64 работает значительно быстрее, чем код x86, поэтому не забудьте установить цель платформы AnyCPU для простого выигрыша. По крайней мере, часть этого ускорения была умением оптимизатора при подъеме целочисленного умножения.

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

Также обратите внимание на свою ошибку в двойном корпусе, вы не должны использовать 1.0f.

+0

Интересно. Разве не было бы «избыточного хранилища, конвертировать в float», чтобы урезать любую дополнительную точность в двойном значении, чтобы соответствовать требованию исключить такую ​​дополнительную точность при явном преобразовании? – phoog

+0

Да, я думаю, что это основная проблема. FPU просто не нравится * float * много. Создание эффективного кода для него - дьявольское черное искусство в целом. В SSE2 значительно улучшилось. –

+0

Очень информативный, спасибо за подробные ответы. Но тогда я не могу понять, почему устранение обратной записи (как говорит ZongZhengLi), эффект исчезает? Я попытался запустить режим Debug, чтобы исключить оптимизацию, когда обратная связь не нужна, и поведение очень похоже на режим Release. – MaMazav