LINUX.ORG.RU

Вещественные числа в Go

 ,


0

3

Вопрос относится к коду. Я написал функцию на Go и на C для вычисления расстояния в метрах между двумя точками. Но на разных языках функция выдаёт разный результат.

Вот код функции на Go:

const earthRadiusM = 6371000

func calculate_distance(lon1, lat1, lon2, lat2 float64) float64 {
	lon_delta := lon2 - lon1

	// convert degrees to radians
	lat1_radians      := lat1 * math.Pi / 180.0
	lat2_radians      := lat2 * math.Pi / 180.0
	lon_delta_radians := lon_delta * math.Pi / 180.0

	return earthRadiusM * math.Acos(math.Sin(lat1_radians) * math.Sin(lat2_radians) + math.Cos(lat1_radians) * math.Cos(lat2_radians) * math.Cos(lon_delta_radians))
}

И на C:

const int EARTH_RADIUS_M = 6371000;

double
calculate_distance
(double lon1, double lat1, double lon2, double lat2)
{
	double lon_delta = lon2 - lon1;

	// convert to radians
	double lat1_radians      = lat1 * M_PI / 180.0;
	double lat2_radians      = lat2 * M_PI / 180.0;
	double lon_delta_radians = lon_delta * M_PI / 180.0;

	return EARTH_RADIUS_M * acos(
		sin(lat1_radians) * sin(lat2_radians) +
		cos(lat1_radians) * cos(lat2_radians) * cos(lon_delta_radians)
	);
}

Вот результат, который они выдают для точек 55.099879, 51.76003 и 55.100524, 51.764685:

$ go run calculate_distance.go 
519.5123039447425
519.5123039447425
$ gcc -lm calculate_distance.c
$ ./a.out 
519.512286164
519.512286164

Почему результат настолько разный? Запускал на x86_64 64-bit. Я проверил на других языках — результат такой же как в C, отличия начинаются после 8 цифры после запятой:

$ php calculate_distance.php 
519.51228616389
519.51228616389
$ node calculate_distance.js 
519.5122861638861
519.5122861638861

Ответ: 1

Полный исходный код: go, c, php, js



Последнее исправление: elonmusk (всего исправлений: 2)

Ответ на: комментарий от Novell-ch

Действительно, точность PI в Go значительно выше. Чтобы в C увеличить точность PI есть константа M_PIl для работы с long double. Но на результат это не повлияло =(

Исходный код с использованием M_PIl:

const int EARTH_RADIUS_M = 6371000;

long double
calculate_distance
(long double lon1, long double lat1, long double lon2, long double lat2)
{
	long double lon_delta = lon2 - lon1;

	// convert degrees to radians
	long double lat1_radians      = lat1 * M_PIl / 180.0;
	long double lat2_radians      = lat2 * M_PIl / 180.0;
	long double lon_delta_radians = lon_delta * M_PIl / 180.0;

	return EARTH_RADIUS_M * acos(
		sin(lat1_radians) * sin(lat2_radians) +
		cos(lat1_radians) * cos(lat2_radians) * cos(lon_delta_radians)
	);
}

Сравнение:

$ gcc -lm m_pi.c
$ ./a.out
519.512286164
$ gcc -D_GNU_SOURCE -lm m_pil.c
$ ./a.out
519.512286164
elonmusk
() автор топика

Похоже, алгоритмы для acos сильно отличаются в libc и Go. У тебя туда попадает число очень близкое к единице, и где-то там в процессе точность улетает.

i-rinat ★★★★★
()
Ответ на: комментарий от i-rinat

Да, похоже. А ещё объясняет, почему код у jollheef (удалённый комментарий) показал 519.5123..., а у меня снова 519.5122...: у нас разные libc (gnu и мак), и алгоритмы acos в них, похоже, тоже разные

elonmusk
() автор топика
Ответ на: комментарий от Deleted

Круто 👍

Результат на чуть-чуть сдвинулся

$ ./a.out 
519.51229705746604292659895918404799886047840118408203125
519.51229705746604292659895918404799886047840118408203125

Т.е. точность PI играет роль. Остальные отличия, похоже, в разнице алгоритмов acos, как сказал i-rinat 🤔

elonmusk
() автор топика

Проблему решил

Разница в алгоритмах acos. Для одного и того же числа функция выдаёт результаты с разной погрешностью:

$ cat test.go 
package main

import (
	"fmt"
	"math"
)

func main() {
	fmt.Printf("Go's math.Acos(): %.64g\n", math.Acos(0.99999999667534597504355009522925001874682493507862091064453125))
}
$ cat test.c 
#include <math.h>
#include <stdio.h>

int main(void) {
	printf("C's acosl(): %.64Lg\n", acosl(0.99999999667534597504355009522925001874682493507862091064453125L));
	return 0;
}
$ go run test.go 
Go's math.Acos(): 8.1543289164986987316297017969191074371337890625e-05
$ gcc test.c 
$ ./a.out 
C's acosl():      8.154328944552912304147555239250584613319716709156637080013751984e-05

Спасибо jollheef и i-rinat :-)

elonmusk
() автор топика
Ответ на: Проблему решил от elonmusk

С, получается, точнее.

$ cat acos.bc

define acos(x) {
    auto val, angle;
    val = sqrt(1 - x^2) / x;
    angle = a(val);
    return angle;
}

scale = 64;
x = 0.99999999667534597504355009522925001874682493507862091064453125;
acos(x);
quit;
$ bc -ql acos.bc
.0000815432894455291230416166203415642992658693785584321571037167

Если сравнить

bc 8.1543289445529123041616620341564299265869378558432157103...
C  8.15432894455291230414...
Go 8.15432891...

anonymous
()
Ответ на: комментарий от anonymous

В википедиях пишут:

With the GNU C Compiler, long double is 80-bit extended precision on x86 processors regardless of the physical storage used for the type (which can be either 96 or 128 bits),

что в Си long double может занимать 80 бит. А в голанге float64 - это ровно 64 бита

den73 ★★★★★
()
Ответ на: комментарий от den73

Далее, я не знаю какова точность в bc, но «99999999667534597504355009522925001874682493507862091064453125» - это 62 десятичных цифры, т.е. не менее чем 62*3=186 бит. Такой литерал не может быть точно прочитан даже в long float. Нужно бы привести литералы в соответствие, чтобы было меньше путаницы. Например, сравнить литерал и то, что выводится (но нужно учитывать неоднозначность представления флоатов в десятичном формате).

den73 ★★★★★
()

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

ugoday ★★★★★
()
Ответ на: комментарий от den73

Есть https://golang.org/pkg/math/big/, в частности, им можно представить Ваши дикие числа:

package main; import ("fmt"; "math/big")

func main() {
 g,base,err := big.ParseFloat(
  "0.99999999667534597504355009522925001874682493507862091064453125",
  10,220,big.ToNearestEven)
 if err != nil { fmt.Println(err) }
 fmt.Println(base,",",g) } 
Напечатает
10 , 0.99999999667534597504355009522925001874682493507862091064453125
Но арккосинусов для big нет - придётся самому написать или найти на гитхабе.

den73 ★★★★★
()
Последнее исправление: den73 (всего исправлений: 5)
Ответ на: комментарий от ugoday

Приблизительность не означает безответственность. Т.е. вопрос, почему результат отличается на разных языках, имеет право на рассмотрение, как и вопрос о точности.

И ещё - считать acos чисел, близких к единице - стрёмно, тут сразу влетаешь в ошибку округления, т.к. реальный интерес тут представляет расстояние от единицы, а оно ограничивается разрядностью. Нужно контролировать точность, а не полагаться на библиотеку. И желательно сделать так, чтобы иметь дело с эпсилон, а не 1+эпсилон. Т.е. для близких точек нужно как-то плясать от арксинуса числа, близкого к нулю. Возможно, разбить формулу на случаи отдельно для близких и отдельно для далёких точек и проследить, чтобы они гладко переходили друг в друга.

den73 ★★★★★
()
Последнее исправление: den73 (всего исправлений: 2)
Ответ на: комментарий от anonymous

Ответ WolframAlpha: 8.154328944552912304161662034156429926586937855843215710316199041224552457393664984654236665142869956049169441183902135995526231173852181084973679593287115088060402315760297978556003844935481566286656372350027966615679189625e-05

  • в C расхождение начинается с 20-го знака после запятой,
  • в Go — с 8-го.
elonmusk
() автор топика

Шесть значимых чисел совпадают, это точность 4-х байтового float. Кто-то недоработал до double.

anonymous
()
Вы не можете добавлять комментарии в эту тему. Тема перемещена в архив.