Я делаю * не * хочу правильное округление для функции exp
Реализация CCC-библиотеки GCC в системах Debian, по-видимому, представляет собой (IEEE 754-2008) -удобную реализацию функции exp
, подразумевая, что округление всегда будет правильным:
(из Википедии) Стандарт с плавающей запятой IEEE гарантирует, что добавление, вычитание, умножение, деление, плавное многократное добавление, квадратный корень и остаток с плавающей запятой даст корректный округленный результат операции бесконечной точности. В 1985 году такая гарантия не предоставлялась для более сложных функций, и они, как правило, с точностью до последнего бит в лучшем случае. Тем не менее, стандарт 2008 гарантирует, что соответствующие реализации дадут правильно округленные результаты, которые учитывают активный режим округления; Однако выполнение функций не является обязательным.
Оказывается, я сталкиваюсь с ситуацией, когда эта функция действительно затрудняет, потому что точный результат функции exp
часто находится почти точно в середине между двумя последовательными значениями double
(1), а затем программа переносит множество дальнейших вычислений, теряя скорость до 400 (!) в скорости: на самом деле это было объяснением моего (проблемного: -S) вопроса # 43530011.
(1) Точнее, это происходит, когда аргумент exp
оказывается имеющим вид (2 k + 1) × 2 -53 с ka довольно малым целым числом (например, 242 например). В частности, вычисления, участвующие в pow (1. + x, 0.5)
, имеют тенденцию называть exp
таким аргументом, когда x
имеет порядок 2 -44.
Так как реализации правильного округления могут быть настолько трудоемкими в определенных обстоятельствах, я думаю, разработчики также разработали способ получить чуть менее точный результат (скажем, только до 0,6 ULP или что-то вроде этого) за время, которое (грубо) ограничено для каждого значения аргумента в заданном диапазоне... (2)
... Но как это сделать?
(2) Я имею в виду, что я просто не хочу, чтобы некоторые исключительные значения аргумента типа (2 k + 1) × 2 -53 были бы намного более трудоемкими, чем большинство значения того же порядка величины; но, конечно, я не возражаю, если некоторые исключительные значения аргумента идут намного быстрее, или если большие аргументы (по абсолютной величине) нуждаются в большем времени вычисления.
Вот минимальная программа, показывающая явление:
#include <stdlib.h>
#include <stdio.h>
#include <math.h>
#include <time.h>
int main (void)
{
int i;
double a, c;
c = 0;
clock_t start = clock ();
for (i = 0; i < 1e6; ++i) // Doing a large number of times the same type of computation with different values, to smoothen random fluctuations.
{
a = (double) (1 + 2 * (rand () % 0x400)) / 0x20000000000000; // "a" has only a few significant digits, and its last non-zero digit is at (fixed-point) position 53.
c += exp (a); // Just to be sure that the compiler will actually perform the computation of exp (a).
}
clock_t stop = clock ();
printf ("%e\n", c); // Just to be sure that the compiler will actually perform the computation.
printf ("Clock time spent: %d\n", stop - start);
return 0;
}
Теперь после gcc -std=c99 program53.c -lm -o program53
:
$ ./program53
1.000000e+06
Clock time spent: 13470008
$ ./program53
1.000000e+06
Clock time spent: 13292721
$ ./program53
1.000000e+06
Clock time spent: 13201616
С другой стороны, с program52
и program54
(полученным заменой 0x20000000000000
соответственно 0x10000000000000
и 0x40000000000000
):
$ ./program52
1.000000e+06
Clock time spent: 83594
$ ./program52
1.000000e+06
Clock time spent: 69095
$ ./program52
1.000000e+06
Clock time spent: 54694
$ ./program54
1.000000e+06
Clock time spent: 86151
$ ./program54
1.000000e+06
Clock time spent: 74209
$ ./program54
1.000000e+06
Clock time spent: 78612
Остерегайтесь, что это явление зависит от реализации!. По-видимому, среди общих реализаций это проявляется только в системах Debian (включая Ubuntu).
P.-S.: Надеюсь, что мой вопрос не дублируется: я искал аналогичный вопрос полностью без успеха, но, возможно, я заметил, что использовать соответствующие ключевые слова...: -/
Ответы
Ответ 1
Чтобы ответить на общий вопрос о том, почему библиотечные функции необходимы для корректного округления результатов:
Плавная точка тяжелая, и часто время не поддается интуиции. Не каждый программист прочитал то, что у них должно было быть. Когда библиотеки использовали несколько слегка неточное округление, люди жаловались на точность библиотечной функции, когда их неточные вычисления неизбежно ошибались и порождали бессмысленность. В ответ авторы библиотек сделали свои библиотеки точно округленными, поэтому теперь люди не могут переложить вину на них.
Во многих случаях конкретные знания о алгоритмах с плавающей запятой могут значительно улучшить точность и/или производительность, например, в тестовом сценарии:
Взятие exp()
чисел, очень близких к 0
в числах с плавающей запятой, проблематично, так как результат - это число, близкое к 1
, а вся точность находится в разнице с одним, поэтому наиболее значимые цифры потеряны. Точнее (и значительно быстрее в этом тестовом сценарии) вычислять exp(x) - 1
через библиотечную функцию C math expm1(x)
. Если сам exp()
действительно необходим, сделать expm1(x) + 1
еще быстрее.
Аналогичная проблема существует и для вычисления log(1 + x)
, для которой существует функция log1p(x)
.
Быстрое исправление, ускоряющее предоставленный тестовый тест:
#include <stdlib.h>
#include <stdio.h>
#include <math.h>
#include <time.h>
int main (void)
{
int i;
double a, c;
c = 0;
clock_t start = clock ();
for (i = 0; i < 1e6; ++i) // Doing a large number of times the same type of computation with different values, to smoothen random fluctuations.
{
a = (double) (1 + 2 * (rand () % 0x400)) / 0x20000000000000; // "a" has only a few significant digits, and its last non-zero digit is at (fixed-point) position 53.
c += expm1 (a) + 1; // replace exp() with expm1() + 1
}
clock_t stop = clock ();
printf ("%e\n", c); // Just to be sure that the compiler will actually perform the computation.
printf ("Clock time spent: %d\n", stop - start);
return 0;
}
В этом случае таймеры на моей машине таковы:
Исходный код
1.000000e + 06
Расписание часов: 21543338
Измененный код
1.000000e + 06
Время в часах: 55076
Программисты, обладающие передовыми знаниями о сопутствующих компромиссах, могут иногда использовать приблизительные результаты, где точность не критична
Для опытного программиста может быть возможно написать аппроксимативную реализацию медленной функции с использованием таких методов, как полиномы Ньютона-Рафсона, Тейлора или Маклаурина, в частности неточно закругленные специальные функции из таких библиотек, как Intel MKL, AMD AMCL, стандартное соответствие компилятора, снижение точности до ieee754 binary32 (float
) или их комбинация.
Обратите внимание, что лучшее описание проблемы обеспечит лучший ответ.
Ответ 2
Относительно вашего комментария к ответе @EOF, "написать свое" замечание от @NominalAnimal кажется достаточно простым здесь, даже тривиальным, следующим образом.
У вашего исходного кода выше есть максимально возможный аргумент для exp() a = (1 + 2 * 0x400)/0x2000... = 4.55e-13 (это действительно должно быть 2 * 0x3FF, и я рассчитываю 13 нулей после 0x2000..., что делает его 2x16 ^ 13). Так что максимальный аргумент 4.55e-13 очень мал.
И тогда тривиальное разложение Тейлора exp (a) = 1 + a + (a ^ 2)/2 + (a ^ 3)/6 +..., которое уже дает вам все двойные точность для таких небольших аргументов. Теперь вам придется отбросить часть 1, как описано выше, а затем просто сводится к expm1 (a) = a * (1. + a * (1. + a/3.)/2.) И это должно пройти довольно быстро! Просто убедитесь, что a остается маленьким. Если он становится немного больше, просто добавьте следующий термин a ^ 4/24 (вы видите, как это сделать?).
<сильное → > РЕДАКТИРОВАТЬ < <
Я изменил программу тестирования OP следующим образом, чтобы проверить немного больше материала (обсуждение следует за кодом)
/* https://stackoverflow.com/questions/44346371/
i-do-not-want-correct-rounding-for-function-exp/44397261 */
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#include <time.h>
#define BASE 16 /*denominator will be (multiplier)xBASE^EXPON*/
#define EXPON 13
#define taylorm1(a) (a*(1.+a*(1.+a/3.)/2.)) /*expm1() approx for small args*/
int main (int argc, char *argv[]) {
int N = (argc>1?atoi(argv[1]):1e6),
multiplier = (argc>2?atoi(argv[2]):2),
isexp = (argc>3?atoi(argv[3]):1); /* flags to turn on/off exp() */
int isexpm1 = 1; /* and expm1() for timing tests*/
int i, n=0;
double denom = ((double)multiplier)*pow((double)BASE,(double)EXPON);
double a, c=0.0, cm1=0.0, tm1=0.0;
clock_t start = clock();
n=0; c=cm1=tm1=0.0;
/* --- to smooth random fluctuations, do the same type of computation
a large number of (N) times with different values --- */
for (i=0; i<N; i++) {
n++;
a = (double)(1 + 2*(rand()%0x400)) / denom; /* "a" has only a few
significant digits, and its last non-zero
digit is at (fixed-point) position 53. */
if ( isexp ) c += exp(a); /* turn this off to time expm1() alone */
if ( isexpm1 ) { /* you can turn this off to time exp() alone, */
cm1 += expm1(a); /* but difference is negligible */
tm1 += taylorm1(a); }
} /* --- end-of-for(i) --- */
int nticks = (int)(clock()-start);
printf ("N=%d, denom=%dx%d^%d, Clock time: %d (%.2f secs)\n",
n, multiplier,BASE,EXPON,
nticks, ((double)nticks)/((double)CLOCKS_PER_SEC));
printf ("\t c=%.20e,\n\t c-n=%e, cm1=%e, tm1=%e\n",
c,c-(double)n,cm1,tm1);
return 0;
} /* --- end-of-function main() --- */
Скомпилируйте и запустите его как test для воспроизведения сценария OP 0x2000... или запустите его с (до трех) необязательных аргументов test # tests timeexp, где #trials по умолчанию используется OP 1000000 и multipler по умолчанию 2 для OP 2x16 ^ 13 (измените его на 4 и т.д. Для других ее тестов). Для последнего аргумента timeexp введите 0, чтобы выполнить только expm1() (и мой ненужный taylor-like) расчет. Дело в том, чтобы показать, что неудачные временные случаи, отображаемые OP, исчезают с expm1(), который принимает "нет времени вообще" независимо от множителя.
Итак, по умолчанию выполняется test и test 1000000 4, произведите (хорошо, я назвал программу округлением)...
bash-4.3$ ./rounding
N=1000000, denom=2x16^13, Clock time: 11155070 (11.16 secs)
c=1.00000000000000023283e+06,
c-n=2.328306e-10, cm1=1.136017e-07, tm1=1.136017e-07
bash-4.3$ ./rounding 1000000 4
N=1000000, denom=4x16^13, Clock time: 200211 (0.20 secs)
c=1.00000000000000011642e+06,
c-n=1.164153e-10, cm1=5.680083e-08, tm1=5.680083e-08
Итак, первое, что вы заметите, это то, что OP cn с использованием exp() существенно отличается от cm1 == tm1, используя expm1(), а мой taylor ок. Если вы уменьшите N, они согласуются, как показано ниже.
N=10, denom=2x16^13, Clock time: 941 (0.00 secs)
c=1.00000000000007140954e+01,
c-n=7.140954e-13, cm1=7.127632e-13, tm1=7.127632e-13
bash-4.3$ ./rounding 100
N=100, denom=2x16^13, Clock time: 5506 (0.01 secs)
c=1.00000000000010103918e+02,
c-n=1.010392e-11, cm1=1.008393e-11, tm1=1.008393e-11
bash-4.3$ ./rounding 1000
N=1000, denom=2x16^13, Clock time: 44196 (0.04 secs)
c=1.00000000000011345946e+03,
c-n=1.134595e-10, cm1=1.140730e-10, tm1=1.140730e-10
bash-4.3$ ./rounding 10000
N=10000, denom=2x16^13, Clock time: 227215 (0.23 secs)
c=1.00000000000002328306e+04,
c-n=2.328306e-10, cm1=1.131288e-09, tm1=1.131288e-09
bash-4.3$ ./rounding 100000
N=100000, denom=2x16^13, Clock time: 1206348 (1.21 secs)
c=1.00000000000000232831e+05,
c-n=2.328306e-10, cm1=1.133611e-08, tm1=1.133611e-08
И что касается времени exp() и expm1(), посмотрите сами...
bash-4.3$ ./rounding 1000000 2
N=1000000, denom=2x16^13, Clock time: 11168388 (11.17 secs)
c=1.00000000000000023283e+06,
c-n=2.328306e-10, cm1=1.136017e-07, tm1=1.136017e-07
bash-4.3$ ./rounding 1000000 2 0
N=1000000, denom=2x16^13, Clock time: 24064 (0.02 secs)
c=0.00000000000000000000e+00,
c-n=-1.000000e+06, cm1=1.136017e-07, tm1=1.136017e-07
Вопрос: вы заметите, что как только вычисление exp() достигает N = 10000, его сумма остается постоянной независимо от более крупного N. Не знаете, почему это произойдет.
→ __ SECOND EDIT __ <
Хорошо, @EOF, "вы заставили меня посмотреть" с вашим комментарием "heirarchical accumulation". И это действительно помогает приблизить сумму exp() ближе (гораздо ближе) к (предположительно правильной) сумме expm1(). Модифицированный код сразу же после обсуждения. Но одно обсуждение здесь: вспомните множитель сверху. Это прошло, и там же expon, так что знаменатель теперь 2 ^ expon, где значение по умолчанию 53, сопоставление OP по умолчанию (и Я считаю, что лучше совместить то, как она думала об этом). Хорошо, и вот код...
/* https://stackoverflow.com/questions/44346371/
i-do-not-want-correct-rounding-for-function-exp/44397261 */
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#include <time.h>
#define BASE 2 /*denominator=2^EXPON, 2^53=2x16^13 default */
#define EXPON 53
#define taylorm1(a) (a*(1.+a*(1.+a/3.)/2.)) /*expm1() approx for small args*/
int main (int argc, char *argv[]) {
int N = (argc>1?atoi(argv[1]):1e6),
expon = (argc>2?atoi(argv[2]):EXPON),
isexp = (argc>3?atoi(argv[3]):1), /* flags to turn on/off exp() */
ncparts = (argc>4?atoi(argv[4]):1), /* #partial sums for c */
binsize = (argc>5?atoi(argv[5]):10);/* #doubles to sum in each bin */
int isexpm1 = 1; /* and expm1() for timing tests*/
int i, n=0;
double denom = pow((double)BASE,(double)expon);
double a, c=0.0, cm1=0.0, tm1=0.0;
double csums[10], cbins[10][65537]; /* c partial sums and heirarchy */
int nbins[10], ibin=0; /* start at lowest level */
clock_t start = clock();
n=0; c=cm1=tm1=0.0;
if ( ncparts > 65536 ) ncparts=65536; /* array size check */
if ( ncparts > 1 ) for(i=0;i<ncparts;i++) cbins[0][i]=0.0; /*init bin#0*/
/* --- to smooth random fluctuations, do the same type of computation
a large number of (N) times with different values --- */
for (i=0; i<N; i++) {
n++;
a = (double)(1 + 2*(rand()%0x400)) / denom; /* "a" has only a few
significant digits, and its last non-zero
digit is at (fixed-point) position 53. */
if ( isexp ) { /* turn this off to time expm1() alone */
double expa = exp(a); /* exp(a) */
c += expa; /* just accumulate in a single "bin" */
if ( ncparts > 1 ) cbins[0][n%ncparts] += expa; } /* accum in ncparts */
if ( isexpm1 ) { /* you can turn this off to time exp() alone, */
cm1 += expm1(a); /* but difference is negligible */
tm1 += taylorm1(a); }
} /* --- end-of-for(i) --- */
int nticks = (int)(clock()-start);
if ( ncparts > 1 ) { /* need to sum the partial-sum bins */
nbins[ibin=0] = ncparts; /* lowest-level has everything */
while ( nbins[ibin] > binsize ) { /* need another heirarchy level */
if ( ibin >= 9 ) break; /* no more bins */
ibin++; /* next available heirarchy bin level */
nbins[ibin] = (nbins[ibin-1]+(binsize-1))/binsize; /*#bins this level*/
for(i=0;i<nbins[ibin];i++) cbins[ibin][i]=0.0; /* init bins */
for(i=0;i<nbins[ibin-1];i++) {
cbins[ibin][(i+1)%nbins[ibin]] += cbins[ibin-1][i]; /*accum in nbins*/
csums[ibin-1] += cbins[ibin-1][i]; } /* accumulate in "one bin" */
} /* --- end-of-while(nprevbins>binsize) --- */
for(i=0;i<nbins[ibin];i++) csums[ibin] += cbins[ibin][i]; /*highest level*/
} /* --- end-of-if(ncparts>1) --- */
printf ("N=%d, denom=%d^%d, Clock time: %d (%.2f secs)\n", n, BASE,expon,
nticks, ((double)nticks)/((double)CLOCKS_PER_SEC));
printf ("\t c=%.20e,\n\t c-n=%e, cm1=%e, tm1=%e\n",
c,c-(double)n,cm1,tm1);
if ( ncparts > 1 ) { printf("\t binsize=%d...\n",binsize);
for (i=0;i<=ibin;i++) /* display heirarchy */
printf("\t level#%d: #bins=%5d, c-n=%e\n",
i,nbins[i],csums[i]-(double)n); }
return 0;
} /* --- end-of-function main() --- */
Хорошо, и теперь вы можете заметить два дополнительных аргумента командной строки после старого timeexp. Они ncparts для исходного количества ящиков, в которые будут распределены все #trials. Таким образом, на самом низком уровне иерархии каждый бин должен (по модулю ошибок) иметь сумму # tests/ncparts. После этого аргумент binsize, который будет состоять из числа удвоений, суммированных в каждом бункере на каждом последующем уровне, до тех пор, пока на последнем уровне меньше (или равно) #bins binsize > . Итак, вот пример, делящий 1000000 проб в 50000 ящиков, что означает 20doubles/bin на самом низком уровне и 5doubles/bin после этого...
bash-4.3$ ./rounding 1000000 53 1 50000 5
N=1000000, denom=2^53, Clock time: 11129803 (11.13 secs)
c=1.00000000000000465661e+06,
c-n=4.656613e-09, cm1=1.136017e-07, tm1=1.136017e-07
binsize=5...
level#0: #bins=50000, c-n=4.656613e-09
level#1: #bins=10002, c-n=1.734588e-08
level#2: #bins= 2002, c-n=7.974450e-08
level#3: #bins= 402, c-n=1.059379e-07
level#4: #bins= 82, c-n=1.133885e-07
level#5: #bins= 18, c-n=1.136214e-07
level#6: #bins= 5, c-n=1.138542e-07
Обратите внимание, что c-n для exp() очень хорошо сходится к значению expm1(). Но обратите внимание, как это лучше всего на уровне №5 и не равномерно сходится. И обратите внимание, если вы сломаете #trialsв только 5000 исходных бункеров, вы получите такой же хороший результат,
bash-4.3$ ./rounding 1000000 53 1 5000 5
N=1000000, denom=2^53, Clock time: 11165924 (11.17 secs)
c=1.00000000000003527384e+06,
c-n=3.527384e-08, cm1=1.136017e-07, tm1=1.136017e-07
binsize=5...
level#0: #bins= 5000, c-n=3.527384e-08
level#1: #bins= 1002, c-n=1.164153e-07
level#2: #bins= 202, c-n=1.158332e-07
level#3: #bins= 42, c-n=1.136214e-07
level#4: #bins= 10, c-n=1.137378e-07
level#5: #bins= 4, c-n=1.136214e-07
Фактически, игра с ncparts и binsize, похоже, не проявляет высокой чувствительности, и это не всегда "лучше" (т.е. меньше для binsize). Поэтому я точно не знаю, что происходит. Может быть ошибка (или два), или может быть еще один вопрос для @EOF...???
→ EDIT - пример, показывающий добавление пары "бинарное дерево", иерархия <
Пример ниже добавлен в соответствии с комментарием @EOF
(Примечание: повторите копию предыдущего кода. Мне пришлось отредактировать вычисления nbins [ibin] для каждого следующего уровня до nbins [ibin] = (nbins [ibin-1] + (binsize-1))/binsize; от nbins [ibin] = (nbins [ibin-1] + 2 * binsize)/binsize;, который был "слишком консервативен" для создания ... 16,8,4, 2)
bash-4.3$ ./rounding 1024 53 1 512 2
N=1024, denom=2^53, Clock time: 36750 (0.04 secs)
c=1.02400000000011573320e+03,
c-n=1.157332e-10, cm1=1.164226e-10, tm1=1.164226e-10
binsize=2...
level#0: #bins= 512, c-n=1.159606e-10
level#1: #bins= 256, c-n=1.166427e-10
level#2: #bins= 128, c-n=1.166427e-10
level#3: #bins= 64, c-n=1.161879e-10
level#4: #bins= 32, c-n=1.166427e-10
level#5: #bins= 16, c-n=1.166427e-10
level#6: #bins= 8, c-n=1.166427e-10
level#7: #bins= 4, c-n=1.166427e-10
level#8: #bins= 2, c-n=1.164153e-10
→ EDIT - показать элегантное решение @EOF в комментарии ниже <
"Добавление пара" может быть изящно выполнено рекурсивно, согласно приведенному ниже комментарию @EOF, который я воспроизвожу здесь. (Обратите внимание на случай 0/1 в конце рекурсии для обработки n четный/нечетный.)
/* Quoting from EOF comment...
What I (EOF) proposed is effectively a binary tree of additions:
a+b+c+d+e+f+g+h as ((a+b)+(c+d))+((e+f)+(g+h)).
Like this: Add adjacent pairs of elements, this produces
a new sequence of n/2 elements.
Recurse until only one element is left.
(Note that this will require n/2 elements of storage,
rather than a fixed number of bins like your implementation) */
double trecu(double *vals, double sum, int n) {
int midn = n/2;
switch (n) {
case 0: break;
case 1: sum += *vals; break;
default: sum = trecu(vals+midn, trecu(vals,sum,midn), n-midn); break; }
return(sum);
}
Ответ 3
Это "ответ" /продолжение в EOF, предшествующий комментариям его алгоритм trecu() и код для его предложения "суммирование двоичного дерева". "Предварительные требования" перед чтением этого читают эту дискуссию. Было бы неплохо собрать все это в одном организованном месте, но я еще этого не сделал...
... То, что я сделал, это собрать EOF trecu() в тестовую программу из предыдущего ответа, который я написал, изменив исходную тестовую программу OP. Но затем я обнаружил, что trecu() генерирует точно (и я имею в виду точно) тот же ответ, что и "простая сумма" c, используя exp(), а не сумма cm1, используя expm1(), которую мы ожидали от более точного суммирования двоичного дерева.
Но эта тестовая программа немного (может быть, две биты:) "свернуты" (или, как сказал EOF, "нечитабельно" ), поэтому я написал отдельную небольшую тестовую программу, приведенную ниже (с примерами прогонов и обсуждением ниже), отдельно тестировать/осуществлять trecu(). Более того, я также написал функцию bintreesum() в приведенный ниже код, который абстрагирует/инкапсулирует итеративный код для суммирования двоичного дерева, который я вложил в предыдущую тестовую программу. В этом предыдущем случае мой итеративный код действительно приблизился к ответу cm1, поэтому я ожидал, что EOF рекурсивный trecu() сделает то же самое. Долгий и короткий - это то, что ниже, то же самое происходит - bintreesum() остается близким к правильному ответу, в то время как trecu() уходит дальше, точно воспроизводя "равную сумму".
То, что мы суммируем ниже, это просто сумма (i), я = 1... n, что является просто известным n (n + 1)/2. Но это не совсем верно - чтобы воспроизвести проблему ОП, слагаемое не является суммой (i) в одиночку, а скорее суммой (1 + я * 10 ^ (- e)), где e можно указать в командной строке. Итак, для, скажем, n = 5, вы не получаете 15, а скорее 5.000... 00015, или для n = 6 вы получаете 6.000... 00021 и т.д. И чтобы избежать длинного длинного формата, я печатаю f ( ) sum-n, чтобы удалить эту целую часть. Хорошо??? Итак, вот код...
/* Quoting from EOF comment...
What I (EOF) proposed is effectively a binary tree of additions:
a+b+c+d+e+f+g+h as ((a+b)+(c+d))+((e+f)+(g+h)).
Like this: Add adjacent pairs of elements, this produces
a new sequence of n/2 elements.
Recurse until only one element is left. */
#include <stdio.h>
#include <stdlib.h>
double trecu(double *vals, double sum, int n) {
int midn = n/2;
switch (n) {
case 0: break;
case 1: sum += *vals; break;
default: sum = trecu(vals+midn, trecu(vals,sum,midn), n-midn); break; }
return(sum);
} /* --- end-of-function trecu() --- */
double bintreesum(double *vals, int n, int binsize) {
double binsum = 0.0;
int nbin0 = (n+(binsize-1))/binsize,
nbin1 = (nbin0+(binsize-1))/binsize,
nbins[2] = { nbin0, nbin1 };
double *vbins[2] = {
(double *)malloc(nbin0*sizeof(double)),
(double *)malloc(nbin1*sizeof(double)) },
*vbin0=vbins[0], *vbin1=vbins[1];
int ibin=0, i;
for ( i=0; i<nbin0; i++ ) vbin0[i] = 0.0;
for ( i=0; i<n; i++ ) vbin0[i%nbin0] += vals[i];
while ( nbins[ibin] > 1 ) {
int jbin = 1-ibin; /* other bin, 0<-->1 */
nbins[jbin] = (nbins[ibin]+(binsize-1))/binsize;
for ( i=0; i<nbins[jbin]; i++ ) vbins[jbin][i] = 0.0;
for ( i=0; i<nbins[ibin]; i++ )
vbins[jbin][i%nbins[jbin]] += vbins[ibin][i];
ibin = jbin; /* swap bins for next pass */
} /* --- end-of-while(nbins[ibin]>0) --- */
binsum = vbins[ibin][0];
free((void *)vbins[0]); free((void *)vbins[1]);
return ( binsum );
} /* --- end-of-function bintreesum() --- */
#if defined(TESTTRECU)
#include <math.h>
#define MAXN (2000000)
int main(int argc, char *argv[]) {
int N = (argc>1? atoi(argv[1]) : 1000000 ),
e = (argc>2? atoi(argv[2]) : -10 ),
binsize = (argc>3? atoi(argv[3]) : 2 );
double tens = pow(10.0,(double)e);
double *vals = (double *)malloc(sizeof(double)*MAXN),
sum = 0.0;
double trecu(), bintreesum();
int i;
if ( N > MAXN ) N=MAXN;
for ( i=0; i<N; i++ ) vals[i] = 1.0 + tens*(double)(i+1);
for ( i=0; i<N; i++ ) sum += vals[i];
printf(" N=%d, Sum_i=1^N {1.0 + i*%.1e} - N = %.8e,\n"
"\t plain_sum-N = %.8e,\n"
"\t trecu-N = %.8e,\n"
"\t bintreesum-N = %.8e \n",
N, tens, tens*((double)N)*((double)(N+1))/2.0,
sum-(double)N,
trecu(vals,0.0,N)-(double)N,
bintreesum(vals,N,binsize)-(double)N );
} /* --- end-of-function main() --- */
#endif
Итак, если вы сохраните его как trecu.c, тогда скомпилируйте его как cc -DTESTTRECU trecu.c -lm -o trecu И затем запустите с нулевым значением до трех необязательных команд- line args как trecu # tests e binsize По умолчанию: # tests = 1000000 (например, программа OP), e = -10 и binsize = 2 (для моей функции bintreesum() выполните двоичный - сумма, а не большие ячейки).
И вот некоторые результаты испытаний, иллюстрирующие проблему, описанную выше,
bash-4.3$ ./trecu
N=1000000, Sum_i=1^N {1.0 + i*1.0e-10} - N = 5.00000500e+01,
plain_sum-N = 5.00000500e+01,
trecu-N = 5.00000500e+01,
bintreesum-N = 5.00000500e+01
bash-4.3$ ./trecu 1000000 -15
N=1000000, Sum_i=1^N {1.0 + i*1.0e-15} - N = 5.00000500e-04,
plain_sum-N = 5.01087168e-04,
trecu-N = 5.01087168e-04,
bintreesum-N = 5.00000548e-04
bash-4.3$
bash-4.3$ ./trecu 1000000 -16
N=1000000, Sum_i=1^N {1.0 + i*1.0e-16} - N = 5.00000500e-05,
plain_sum-N = 6.67552231e-05,
trecu-N = 6.67552231e-05,
bintreesum-N = 5.00001479e-05
bash-4.3$
bash-4.3$ ./trecu 1000000 -17
N=1000000, Sum_i=1^N {1.0 + i*1.0e-17} - N = 5.00000500e-06,
plain_sum-N = 0.00000000e+00,
trecu-N = 0.00000000e+00,
bintreesum-N = 4.99992166e-06
Итак, вы можете видеть, что для запуска по умолчанию, e = -10, все делают все правильно. То есть, верхняя строка, которая говорит "Sum", просто делает n (n + 1)/2, поэтому, вероятно, отображается правильный ответ. И все ниже этого согласны на тестовый случай e = -10 по умолчанию. Но для случаев e = -15 и e = -16 ниже, trecu() точно согласуется с plain_sum, а bintreesum остается довольно близким к правильному ответу. И, наконец, для e = -17, plain_sum и trecu() "исчезли", в то время как bintreesum() все еще хорошо висит там.
Итак, trecu() правильно делает сумму правильно, но ее рекурсия, по-видимому, не делает этого типа "двоичного дерева", что мой более простой итеративный bintreesum(), по-видимому, делает правильно. И это действительно демонстрирует, что предложение EOF для "суммирования двоичного дерева" реализует довольно значительное улучшение по сравнению с plain_sum для этих случаев с 1 + epsilon. Поэтому нам бы очень хотелось увидеть его рекурсивную работу trecu()!!! Когда я впервые посмотрел на него, я подумал, что это сработало. Но эта двойная рекурсия (для этого есть специальное имя?) В его случае по умолчанию:, по-видимому, более запутанным (по крайней мере для меня:), чем я думал. Как я уже сказал, он выполняет сумму, но не "двоичное дерево".
Хорошо, так кто хотел бы взять вызов и объяснить, что происходит в этой рекурсии trecu()? И, возможно, что более важно, исправить это, чтобы он сделал то, что предназначалось. Спасибо.