Суммирование потенциальной энергии в for (int i = 0; ...) {for (int j = 0; ...) {summation}}, вложенном в цикл, не работает - PullRequest
0 голосов
/ 28 сентября 2018

Вот простой код, который воспроизводит полученную ошибку:

#include <math.h> 
#include <iostream>
//#include <omp.h>
//handling Not a number exception:
#include <fenv.h>
#include <signal.h>
#include "unistd.h"

void handler(int sig)
{
  printf("Floating Point Exception\n");
  exit(0);
}
#define EKCOR
const float alpha=200.0/137;
const int N=4096;//4096;//8192;//16384;
const float md=940;
const float Ep=0.1f;
float E1;
int STEP=1;
struct float3
{
  float x, y, z;
};
float3 Pi;
struct Particle
{
  float x;
  float y;
  float z;
  float t;
  float vx;
  float vy;
  float vz;
  float m;
};
Particle p[N] __attribute__((aligned(64)));
inline float3 RandomDirection()
{
  float number1 = static_cast <float> (rand())/(static_cast <float> (RAND_MAX));
  float z   = 2.0*number1 - 1.;  
  float rho = sqrtf((1.+z)*(1.-z));
  float number2 = static_cast <float> (rand())/(static_cast <float> (RAND_MAX));
  float phi = M_PI*2.0*number2;
  float3 result={rho*cosf(phi), rho*sinf(phi), z};
  return result;
}
void function()
{
  float K=0.0;
  Pi={0.0, 0.0, 0.0};
  double Px=0.0;
  double Py=0.0;
  double Pz=0.0;
  float P3=0.0;
  float P4=0.0;
  //#1
  for(int i=0; i<N; ++i)
  {
    Px+=p[i].vx*p[i].m;
    Py+=p[i].vy*p[i].m;
    Pz+=p[i].vz*p[i].m;
    float Penergy=0.0;
  #pragma novector
    for(int j=0; j<N; ++j) if(i!=j)
    {
      float rdist1=sqrt((p[i].x-p[j].x)*(p[i].x-p[j].x)+(p[i].y-p[j].y)*(p[i].y-p[j].y)+(p[i].z-p[j].z)*(p[i].z-p[j].z));
      Penergy+=alpha/rdist1;
      P4+=alpha/rdist1;
    }
    P3+=Penergy;
    float v2=p[i].vx*p[i].vx+p[i].vy*p[i].vy+p[i].vz*p[i].vz;
    K+=p[i].m*v2/2;
  }
  P4/=2;
  Pi.x=Px;
  Pi.y=Py;
  Pi.z=Pz;
  P3/=2;
  float E2=K+P3;
  float r=(E1-P3)/K;
  std::cout<<"r="<<r<<",E1="<<E1<<",P3="<<P3<<",K="<<K<<std::endl;
  float rc=sqrt(r);
  std::cout<<"E2="<<K+P3<<",K="<<K<<",P3="<<P3<<",P4="<<P4<<",Px="<<Pi.x<<",Py="<<Pi.y<<",Pz="<<Pi.z<<std::endl;
}
void init()
{
  const double pi=3.1415926536;   
  float RADIUS=pow(50.0*N,1.0/3.0);
  Pi={0.0, 0.0, 0.0};
  double Px=0.0, Py=0.0, Pz=0.0;
#pragma omp single
  for(int i=0; i<N; ++i)
  {
    float DISTANCE=0.0f;
    if(i>0)
    {
      while(DISTANCE<=1.0f)
      {
        float theta=pi*static_cast <float> (rand())/(static_cast <float> (RAND_MAX));
        float phi=2*pi*static_cast <float> (rand())/(static_cast <float> (RAND_MAX));
        float rr=RADIUS*static_cast <float> (rand())/(static_cast <float> (RAND_MAX));       
        p[i].x =rr*sin(theta)*cos(phi);
        p[i].y =rr*sin(theta)*sin(phi);
        p[i].z =rr*cos(theta);
        DISTANCE=10000.0f;
      #pragma simd reduction(min:DISTANCE)     
        for(int j=0; j<i; ++j)
        {
          float dij=sqrt((p[i].x-p[j].x)*(p[i].x-p[j].x)+(p[i].y-p[j].y)*(p[i].y-p[j].y)+(p[i].z-p[j].z)*(p[i].z-p[j].z));
          if(dij<DISTANCE) DISTANCE=dij;
        }
      }
    }
    else
    {
      float theta=pi*static_cast <float> (rand())/(static_cast <float> (RAND_MAX));
      float phi=2*pi*static_cast <float> (rand())/(static_cast <float> (RAND_MAX));
      float rr=RADIUS*static_cast <float> (rand())/(static_cast <float> (RAND_MAX));       
      p[i].x =rr*sin(theta)*cos(phi);
      p[i].y =rr*sin(theta)*sin(phi);
      p[i].z =rr*cos(theta);
    }
    float modv=sqrt(2.0*Ep/md);
    float3 res=RandomDirection();
    float3 v;
    v.x=modv*res.x;
    v.y=modv*res.y;
    v.z=modv*res.z; 
    p[i].vx =v.x;
    p[i].vy =v.y;
    p[i].vz =v.z;
    p[i].m=md;
    Px+=p[i].vx*p[i].m;
    Py+=p[i].vy*p[i].m;
    Pz+=p[i].vz*p[i].m;   
  }
  Px/=N;
  Py/=N;
  Pz/=N;
#pragma novector
  for(int i=0; i<N; ++i)
  {
    p[i].vx-=Px/p[i].m;
    p[i].vy-=Py/p[i].m;
    p[i].vz-=Pz/p[i].m;
  }
  Px=0.0, Py=0.0, Pz=0.0;
  float K1=0.0;
  float P1=0.0;
  float P2=0.0;
  //#2
#pragma novector
  for(int i=0; i<N; ++i)
  {
    Px+=p[i].vx*p[i].m;
    Py+=p[i].vy*p[i].m;
    Pz+=p[i].vz*p[i].m;
    K1+=p[i].vx*p[i].vx+p[i].vy*p[i].vy+p[i].vz*p[i].vz;
    float pp=0.0;
  #pragma novector
    for(int j=0; j<N; ++j) if(i!=j)
    {
       float rd=sqrt((p[i].x-p[j].x)*(p[i].x-p[j].x)+(p[i].y-p[j].y)*(p[i].y-p[j].y)+(p[i].z-p[j].z)*(p[i].z-p[j].z));
       P1+=alpha/rd;
       pp+=alpha/rd;
    }
    P2+=pp;
  }
  Pi.x=Px;
  Pi.y=Py;
  Pi.z=Pz;
  K1*=md/2;
  P1/=2;
  P2/=2;
  E1=K1+P1;
  std::cout<<"INIT Px="<<Pi.x<<" Py="<<Pi.y<<" Pz="<<Pi.z<<" K1="<<K1<<" P1="<<P1<<" P2="<<P2<<" E1="<<E1<<std::endl;
}

int
main(int argc, char **argv)
{
  //handling Not a number exception:
  feenableexcept(FE_INVALID | FE_DIVBYZERO | FE_OVERFLOW | FE_UNDERFLOW);
  signal(SIGFPE, handler);
  //
  init();
  function();
 }

При N <1024 P1 = P2 и P3 = P4.Только при N = 256 существует небольшая разница: </p>

N = 256 P1 = 3492,48 P2 = 3492,5 P3 = 3492,5 P4 = 3492,48

Но при N = 1024 иN> 1024 разница становится все более и более:

N = 1024 P1 = 34968,6 P2 = 34969,7 P3 = 34969,7 P4 = 34968,6
N = 2048 P1 = 114493 P2 = 114482 P3 = 114482 P4= 114493
N = 4096 P1 = 357880 P2 = 362032 r = -9.14142

Здесь происходит сбой программы, поскольку r = -9.14142 и sqrt (r) выдают исключение с плавающей запятой.

Моя ОС - Fedora 23, процессор - Intel Core i7-3770, и я использовал компиляторы gcc версии 5.3.1 и компилятор intel c ++ icpc версии 17.0.1, если это необходимо.Они оба выдавали ошибку, даже если не использовать OpenMP.

Описание проблемы приводится ниже кода.Мой вопрос:

  1. Почему P1 отличается от P2, а P3 отличается от P4 так хорошо при N> = 1024 (можно скомпилировать с компилятором Intel (icpc) или gcc (g ++) без аргументов)?Программа работает в 1 потоке. Они должны иметь одинаковое значение!

  2. Мне нужно написать код так, чтобы вложенные циклы # 1 и # 2 были распараллелены с использованием

    # pragma omp параллельно для сокращения (+: P) для (int i = 0; i (p [i] .xp [j] .x) + (p [i] .yp [j] .y) (p [я] .yp [J] .y) + (р [я] .zp [J] .z) * (р [я] .zp [J] .z));PP + = альфа / г;} P + = PP;} P / = 2;

    и работал со всеми флагами оптимизации (я использую набор -DCMAKE_CXX_FLAGS = "- march = native -mtune = native -ipo16 -fp-model fast = 2 -O3 -qopt-report= 5 -mcmodel = large "для компилятора Intel).Я не могу этого сделать (даже только с "-O0").Может быть из-за ошибки 1), это дает мне неправильное значение.

Ответы [ 3 ]

0 голосов
/ 28 сентября 2018

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

Три указателя на методы для улучшения потери точности:

  1. Заказатьэлементы увеличиваются в размерах - это может быть слишком дорого, если они еще не отсортированы.
  2. Парное суммирование
  3. Суммирование Кахана
0 голосов
/ 28 сентября 2018

Обратите внимание, что, хотя в теоретическом смысле P1 должен равняться P2, а P3 должен равняться P4, это переменные с плавающей запятой.Более того, они являются переменными с плавающей точкой одинарной точности.В зависимости от порядка вычислений вы обязательно получите разные результаты.Ошибки накапливаются при каждом вычислении из-за неточной природы представления с плавающей запятой.

Пожалуйста, посмотрите и запустите следующий код (tst_float.cpp):

/* g++ -Wall tst_float.cpp -o tst_float && ./tst_float */

#include <stdio.h>

int main()
{
    int ok;
    int i;
    float x;

    x = 0.0;
    for (i = 0; i < 10; ++i) {
        x += 0.1;
    }

    ok = x == 1.0;

    if (ok) {
        printf("ok!\n");
    } else {
        printf("uh-uh?\n");
    }
    printf("x == %10.9f\n", x);

    return 0;
}

Я получаю:

$ g++ -Wall tst_float.cpp -o tst_float && ./tst_float
uh-uh?
x == 1.000000119

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

0 голосов
/ 28 сентября 2018

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

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

(...)

Вот один пример неточности, которая может закрасться:

float f = 0.1f;
float sum;
sum = 0;

for (int i = 0; i < 10; ++i)
    sum += f;
float product = f * 10;
printf("sum = %1.15f, mul = %1.15f, mul2 = %1.15f\n",
        sum, product, f * 10);

Этот код пытается вычислить 'один'тремя разными способами: повторное добавление и два небольших варианта умножения.Естественно, мы получаем три разных результата, и только один из них равен 1,0:

sum=1.000000119209290, mul=1.000000000000000,  mul2=1.000000014901161

(...)

Вот точные значения для 0.1, float(0.1) и double (0.1):

==========================================================================
| Number     | Value                                                     |
|------------|-----------------------------------------------------------|
| 0.1        | 0.1 (of course)                                           |
| float 0.1  | 0.100000001490116119384765625                             |
| double 0.1 | 0.1000000000000000055511151231257827021181583404541015625 |
==========================================================================

После того, как все решено, давайте посмотрим на результаты кода выше:

  1. sum =1.000000119209290: этот расчет начинается с округленного значения, а затем складывается из него десять раз с потенциальным округлением при каждом добавлении, так что есть много места для ошибки, чтобы закрасться. Окончательный результат не равен 1,0, и он не равен 10 * float (0,1).Однако это следующее представимое значение с плавающей запятой выше 1,0, поэтому оно очень близко.
  2. mul = 1.000000000000000: этот расчет начинается с округленного значения, а затем умножается на десять, поэтому вероятность появления ошибки может быть меньше.Оказывается, что преобразование из 0,1 в число с плавающей запятой (0,1) округляется в большую сторону, но умножение на десять происходит, в данном случае, с округлением в меньшую сторону, а иногда два округления дают право. Таким образом, мы получаем правильный ответ по неправильным причинам.Или, возможно, это неправильный ответ, поскольку на самом деле это не десятикратное число с плавающей запятой (0,1)!
  3. mul2 = 1.000000014901161: этот расчет начинается с округленного значения, а затем double -точность умножить на десять, что позволит избежать любой последующей ошибки округления.Таким образом, мы получаем другой правильный ответ - точное значение 10 * float (0.1) (которое может быть сохранено в double, но не в float).

Итак, ответ один неверный, но это всего лишь один float прочь.Второй ответ является правильным (но неточным), тогда как третий ответ является полностью правильным (но кажется неправильным).

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

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

  1. Увеличьте число значащих бит в вашей плавающей запятой.float в C ++ имеют 21 значащий бит (примерно 7 значащих цифр) и double имеют 52 значащих бита (примерно ~ 17 значащих цифр)
  2. Уменьшитьколичество вычислений (так что 4.0*c точнее, чем c+c+c+c)
  3. Попробуйте гарантировать, что вы будете точно такие же вычисления в точно такой же порядок (только тогда вы можете == / != два значения и получить разумный результат)

Итак,Например, если вы измените свой код float с (7 цифр точности) на double с (17 цифр точности), вы увидите, что ваши результаты становятся более точными , а показывает больше цифр.Если вы попытаетесь использовать распараллеливание в своем коде, ваши вычисления могут (или не могут, в зависимости от реализации) выполняться в разных порядках в разных потоках / ядрах, что приводит к дико различной точности с плавающей запятой для каждого участвующего числа.

В качестве примера, вот код рандомасии, использующий double вместо float s:

  double f = 0.1;
  double sum;
  sum = 0;

  for (int i = 0; i < 10; ++i)
      sum += f;
  double product = f * 10;
  printf("sum = %1.15f, mul = %1.15f, mul2 = %1.15f\n",
          sum, product, f * 10);

, который выводит:

  sum = 1.000000000000000, mul = 1.000000000000000, mul2 = 1.000000000000000

Что может показаться правильным, но когда вы увеличиваететочность printf от 1.15f до 1.17f:

  sum = 0.99999999999999989, mul = 1.00000000000000000, mul2 = 1.00000000000000000

Опять же, вы можете видеть, что неточность закралась. sum выполнили 10 операций +, тогда как mul и mul2 сделал одну операцию * каждая, поэтому sum неточность больше, чем неточность двух других.

Если даже 17 цифр точности вам не достаточно, то вас может заинтересоватьрешения произвольная точность для C ++.

Определение BigNum из википедии :

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

(...)

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

Опять акцент мой .

Вот связанный ответ, предлагающий библиотеку BigNum для C ++ :

Арифметическая библиотека GNU Multiple Precision делает то, что вы хотите http://gmplib.org/

Вот предыдущийкод, реализованный с использованием GMP (с использованием 64-битной точности или примерно 21 значащей цифры):

 // Compile like: g++ question.cpp -o out.tmp -lgmpxx -lgmp
 #include <stdio.h>
 #include <gmpxx.h>

 int main(){
      mpf_class f("0.1", 64);
      mpf_class sum("0", 64);

      for (int i = 0; i < 10; ++i)
          sum += f;
      mpf_class product = f * 10;
      printf("sum = %1.17f, mul = %1.17f, mul2 = %1.17f\n",
             sum.get_d(), product.get_d(), ((mpf_class) (f * 10)).get_d());
 }

Что выводит:

  sum = 0.99999999999999989, mul = 0.99999999999999989, mul2 = 0.99999999999999989

Что является результатом выполнения вычисления в 64 biточности, а затем округлить до 51 бита (C ++ double) и распечатать его.

Однако вы можете распечатать значение непосредственно из GMP:

 // Compile like: g++ question.cpp -o out.tmp -lgmpxx -lgmp
 #include <stdio.h>
 #include <gmpxx.h>
 #include <string>

 int main(){
      mpf_class f("0.1", 64);
      mpf_class sum("0", 64);

      for (int i = 0; i < 10; ++i)
          sum += f;
      mpf_class product = f * 10;
      long exp = 10;
      int base = 10;
      int digits = 21;
      printf("sum = %s, mul = %s, mul2 = %s\n",
             sum.get_str(exp, base, digits).c_str(),
             product.get_str(exp, base, digits).c_str(),
             ((mpf_class) (f * 10)).get_str(exp, base, digits).c_str());
 }

Что выводит:

      sum = 1, mul = 1, mul2 = 1

Что является более точным результатом, чем doubleпредставление.Вы можете проверить интерфейс GMP C ++ здесь и здесь . Обратите внимание, однако, что библиотеки произвольной точности, как правило, медленнее, чем встроенные float с или double с. Преимущество состоит в том, что для повышения точности вам просто нужно изменить строку mpf_class variable(expression, precision);.

Также не забудьте проверить предложение PaulMcKenzie Переполнение стека: не работает ли математика с плавающей запятой? :

Вопрос:

Рассмотримследующий код:

0.1 + 0.2 == 0.3 -> false

0.1 + 0.2 -> 0.30000000000000004

Почему происходят эти неточности?

Ответ:

Бинарная математика с плавающей точкой такая.В большинстве языков программирования он основан на стандарте IEEE 754. (...) Суть проблемы в том, что числа представляются в этом формате как целое число, умноженное на два; рациональные числа (например, 0,1, что составляет 1/10) , знаменатель которого не является степенью двойки, не может быть точно представлено .

Константы 0.2 и 0.3 в вашей программе также будут приблизительными к их истинным значениям.Бывает, что ближайший double к 0.2 больше rational число 0.2, но что ближайший double до 0.3 меньше rational число 0.3.Сумма 0.1 и 0.2 оказывается больше rational число 0.3 и, следовательно, не согласен с константой в вашем коде.

Акцент и разметка мои .

...