Что быстрее на esp8266: 64-битная математика или математика с плавающей запятой?
Мне нужно сделать некоторые вычисления с переменными значениями от датчика положения (акклерометров и гироскопов) на esp8266. int32_t математика с этими переменными не имеет достаточного диапазона, а математические операции с плавающей запятой невероятно медленные.
Я надеюсь использовать формат с фиксированной запятой с 24 битами слева от десятичной запятой и 8 битами справа. Таким образом, число будет сохранено в int32_t как 0xffffff.ff. Типичной операцией было бы умножение типа ((i64) x * y) >> 24).
Итак, необходима 64-битная математика. Будет ли это значительно медленнее, чем умножение с плавающей запятой?
@mark-hahn, 👍11
Обсуждение2 ответа
Вычисление относительной производительности 64-битного целого числа по сравнению с умножением с плавающей запятой немного сложнее, чем может показаться.
Легко рассчитать время цикла, который выполняет тысячи вычислений, но если вы не сделаете что-то с результатом, компилятор с радостью их оптимизирует. Заманчиво умножать константы, но компилятор также с радостью оптимизирует это.
Чтобы сделать код как можно более простым и не вводить в цикл ничего, что заняло бы больше времени, вам необходимо отключить или обойти оптимизацию компилятора. Самый простой способ сделать это - использовать модификатор volatile
для переменных. volatile
сообщает компилятору, что он не может предположить, что переменная не изменилась, поэтому компилятор отключит оптимизацию, такую как кэширование переменной в регистре или заметив, что одно и то же вычисление выполняется снова и снова, и просто делает это один раз.
Используя этот метод, с помощью нескольких тестов, чтобы убедиться, что компилятор будет выполнять оптимизации, которые влияют на синхронизацию, я получаю следующие результаты:
non-volatile uint32_t microseconds 1
constants uint32_t microseconds 5
uint16_t microseconds 162513
uint32_t microseconds 137513
uint64_t microseconds 1287533
int16_t microseconds 337513
int32_t microseconds 300023
int64_t microseconds 1287513
float microseconds 912529
64-разрядное умножение без знака или знака занимает примерно в 1,4 раза больше времени, чем умножение с плавающей запятой.
Первый тест подтвердил, что энергонезависимые переменные оптимизированы. Второй тест подтвердил, что умножение констант также оптимизировано.
Отключение Wi-Fi также помогает сохранить результаты тестирования неизменными.
#include <Arduino.h>
#ifdef ESP32
#include <WiFi.h>
#else
#include <ESP8266WiFi.h>
#endif
#define TIMES_TO_LOOP 1000000
volatile uint16_t ux16, uy16, uresult16;
volatile uint32_t ux32, uy32, uresult32;
volatile uint64_t ux64, uy64, uresult64;
volatile int16_t x16, y16, result16;
volatile int32_t x32, y32, result32;
volatile int64_t x64, y64, result64;
volatile float xf, yf, resultf;
uint32_t x32n, y32n, result32n;
uint16_t seed16() {
return random(0, 0xffff);
}
uint32_t seed32() {
return random(0, 0xffffffff);
}
uint64_t seed64() {
return seed32();
}
float seedfloat() {
float x, y;
x = seed32();
y = seed32();
return x / y;
}
void setup() {
uint32_t i;
uint64_t micros_start, micros_end;
Serial.begin(115200);
Serial.println("hello world");
WiFi.mode( WIFI_OFF );
delay(1000);
x32n = seed32();
y32n = seed32();
micros_start = micros();
for(i = 0; i < TIMES_TO_LOOP; i++)
result32n = x32n * y32n;
micros_end = micros();
Serial.print("non-volatile uint32_t microseconds ");
Serial.println(micros_end - micros_start);
micros_start = micros();
for(i = 0; i < TIMES_TO_LOOP; i++)
result32n = 540000 * 15;
micros_end = micros();
Serial.print("constants uint32_t microseconds ");
Serial.println(micros_end - micros_start);
ux16 = seed16();
uy16 = seed16();
x16 = ux16;
y16 = uy16;
micros_start = micros();
for(i = 0; i < TIMES_TO_LOOP; i++)
uresult16 = ux16 * uy16;
micros_end = micros();
Serial.print("uint16_t microseconds ");
Serial.println(micros_end - micros_start);
ux32 = seed32();
uy32 = seed32();
x32 = ux32;
y32 = uy32;
micros_start = micros();
for(i = 0; i < TIMES_TO_LOOP; i++)
uresult32 = ux32 * uy32;
micros_end = micros();
Serial.print("uint32_t microseconds ");
Serial.println(micros_end - micros_start);
ux64 = seed64();
uy64 = seed64();
x64 = ux64;
y64 = uy64;
micros_start = micros();
for(i = 0; i < TIMES_TO_LOOP; i++)
uresult64 = ux64 * uy64;
micros_end = micros();
Serial.print("uint64_t microseconds ");
Serial.println(micros_end - micros_start);
micros_start = micros();
for(i = 0; i < TIMES_TO_LOOP; i++)
result16 = x16 * y16;
micros_end = micros();
Serial.print("int16_t microseconds ");
Serial.println(micros_end - micros_start);
micros_start = micros();
for(i = 0; i < TIMES_TO_LOOP; i++)
result32 = x32 * y32;
micros_end = micros();
Serial.print("int32_t microseconds ");
Serial.println(micros_end - micros_start);
micros_start = micros();
for(i = 0; i < TIMES_TO_LOOP; i++)
result64 = x64 * y64;
micros_end = micros();
Serial.print("int64_t microseconds ");
Serial.println(micros_end - micros_start);
xf = seedfloat();
yf = seedfloat();
micros_start = micros();
for(i = 0; i < TIMES_TO_LOOP; i++)
resultf = xf * yf;
micros_end = micros();
Serial.print("float microseconds ");
Serial.println(micros_end - micros_start);
}
void loop() {
}
Я прогнал ваш код, и результаты совпали с моими. Float * работает * быстрее, чем 64-разрядная версия. int64_t микросекунд 650010 микросекунды с плавающей запятой 456266, @mark-hahn
Обратите внимание, что 64 x 64 -> 64-разрядное умножение обычно в 4 раза медленнее, чем 32 x 32 -> 64-разрядное умножение, которое было в исходном вопросе., @jpa
Я не сомневаюсь в этом, но это немного удивительно. Например: почему int16_t
такой медленный по сравнению с uint16_t
? Умножение - это одна и та же операция для этих двух типов. Аналогично для int32_t
и uint32_t
. Действительно странно. Возможно ли, что "volatile" является слишком пессимистичным в том смысле, что он также предотвращает оптимизацию сокращения мощности, тогда как в реальном приложении они были бы доступны?, @Daniel Wagner
@jpa - Если вы присмотритесь, то увидите, что мультипликация 64x64-битная, а не 32x32., @mark-hahn
@mark-hahn Тогда у вас есть другой способ реализации математики с фиксированной точкой, чем я привык. Компиляторы C достаточно умны, чтобы знать, что если у вас есть 32-разрядная переменная и вы приводите ее к 64 битам непосредственно перед умножением, то старшие биты равны нулю и их не нужно обрабатывать., @jpa
Хорошо, я взял на себя труд закодировать процедуры с фиксированной запятой и протестировать их. Вот этот код ...
// процедуры с фиксированной запятой для 20-битного целого числа / 12-битной дроби
// формат 0xfffff.fff, максимальное значение равно +-524288.9998
#define FP_I 20 // целых битов, например, 0xfffff.0
#define FP_F 12 // бит дроби, например, 0x0.fff
#define fpMul(_x,_y) ( (i32)( ((i64) _x * _y) >> FP_F) )
#define fpDiv(_x,_y) ( (i32)( (((i64) _x) << 32) \
/ (((i64) _y) >> FP_I) ) )
#define decToFp(_dec) ((i32) (_dec * 4096)) // например, decToFp(-1.2)
#define fpToI32(_fp) ((i32) (_fp / 4096))
Я выполнил 1000 циклов и использовал счетчик циклов в качестве аргумента операции. Таким образом, каждый цикл состоял из одной операции плюс одного приращения с плавающей запятой. Я сделал это для умножения с фиксированной запятой, умножения с плавающей запятой, деления с фиксированной запятой и деления с плавающей запятой.
Результаты были шокирующими. Умножение с фиксированной запятой заняло почти столько же времени, сколько и умножение с плавающей запятой. Разделение с фиксированной запятой заняло на 60% больше времени, чем разделение с плавающей запятой. Я просмотрел свой код и перепробовал все, что смог придумать. Я построил его в режиме отладки и выпуска. Я пробовал 80 МГц и 160 МГц. Я нашел скорости с плавающей запятой в Интернете, и мои тесты с плавающей запятой точно соответствовали им.
Я в недоумении относительно того, как 64-битная математика может быть медленнее, чем с плавающей запятой. И то, и другое реализовано в программном обеспечении. Я уверен, что число с плавающей запятой сильно оптимизировано с помощью некоторых хитростей и закодировано на машинном языке. Но мой код с фиксированной запятой - это просто сдвиги и 64-битное умножение / деление. Я не думаю, что есть какие-то уловки, которые можно было бы использовать для этого.
По крайней мере, это облегчает принятие решения. Я предполагаю, что буду использовать с плавающей запятой как можно реже и скрещу пальцы, чтобы мое приложение работало достаточно быстро.
Я надеюсь, что это будет полезно некоторым проходящим мимо гуглерам. Я ничего не смог найти, когда погуглил.
Я не отключал Wi-Fi, но я использовал все другие рекомендации, которые у вас есть здесь. Я играл с этим весь день, и было трудно сделать все правильно, но я вполне уверен в результатах. Тем более, что мои результаты float совпали с результатами в Интернете от нескольких других людей, которые проделали большую работу над этим материалом. Если кто-нибудь захочет вставить мой код с фиксированной запятой в свои тесты скорости, дайте мне знать, совпадают ли ваши результаты с моими. Я и так потратил на это больше времени, чем следовало бы., @mark-hahn
Эй, это не форум, где мы играем в пинг-понг с ответами. Когда вы пишете ответ, он должен содержать решение проблемы, изложенной в вопросе. Никаких "туда-сюда", пожалуйста. Просто вопросы и самодостаточные ответы. Если вы считаете, что решили свою собственную проблему, вам следует объединить все свои ответы. Если нет, то вы можете добавить дополнительную информацию к своему вопросу. И, пожалуйста, убедитесь, что другие все еще могут понять ваш вопрос / ответ, чтобы они тоже могли извлечь из него уроки, @chrisl
И добро пожаловать в Arduino Stack Exchange! Спасибо за обновление., @Nick Gammon
Я не понимаю. Каждый форум, которым я пользовался раньше, был сплошным пинг-понгом. Где мы должны провести эту дискуссию?, @mark-hahn
Рассматривали ли вы возможность реализации 32-битного умножения как умножения 2-значных базовых чисел 2 ^ 16? Что-то вроде [этого] (https://gist.github.com/dmwit/c0abc5995739bc750b8e2d85c7166b4e ). Это может быть быстрее, чем преобразование обоих 32-разрядных чисел в 64-разрядные числа перед умножением. Умножение со знаком также возможно с двумя дополнительными условными обозначениями, если это действительно важно (я думаю, что это не так?)., @Daniel Wagner
...и если умножения двух 30-битных чисел для получения 60-битного числа достаточно для вашего приложения, вы можете уменьшить 4 дешевых умножения из моей предыдущей ссылки до 3, используя алгоритм умножения Карацубы., @Daniel Wagner
@mark-hahn: SE * - это не * форум, это сайт вопросов и ответов. Вы заметите, что интерфейс структурирован по-разному, и социальные соглашения тоже отличаются. [обзор сайта] (/tour) подчеркивает некоторые различия, как и наша страница справочного центра, посвященная [как написать хороший ответ] (/help/how-to-answer ). В любом случае, для вопросов, которые действительно требуют обсуждения, подходящее место для этого - либо здесь, в комментариях, либо (особенно, если обсуждение становится очень долгим или уходит от темы) в [чат] (https://chat.stackexchange.com/?tab=site&host=arduino.stackexchange.com )., @Ilmari Karonen
Кстати, ваша реализация fpDiv
выглядит для меня немного шаткой: например, fpDiv (decToFp (10), decToFp (2))
дает деление на нулевую ошибку. Я думаю, обычно вы хотели бы сделать правильный сдвиг _после_ деления. (Кроме того, я не знаю, так ли это на esp8266, но, по крайней мере, для некоторых платформ / компиляторов / настроек оптимизации установка FP_F
на значение, кратное "собственному" размеру слова / a, например, 8, 16 или 32 бита, может привести к несколько более быстрому кодированию. Это, по крайней мере, стоило бы попробовать.), @Ilmari Karonen
Просто используйте регистр CCOUNT в ядре процессора Xtensa для измерения., @rsaxvc
- ESP8266-01 неправильные настройки управления потоком
- Почему ESP8266 медленнее, чем Arduino nano?
- Как читать и записывать EEPROM в ESP8266
- Как сделать выводы Tx и Rx на ESP-8266-01 в выводах GPIO?
- Как навсегда изменить скорость передачи данных ESP8266 (12e)?
- Как заставить 5-вольтовое реле работать с NodeMCU
- Как исправить: Invalid conversion from 'const char*' to 'char*' [-fpermissive]
- ESP8266 не подключается к Wi-Fi
выполните 10 000 операций каждого ... раза, когда результаты, @jsotola
Кстати, если я был отвергнут, потому что ответ казался очевидным, тогда они должны прочитать остальную часть этой темы., @mark-hahn
Если вам нужны быстрые операции с плавающей запятой с одинарной точностью, рассмотрите ESP32 вместо ESP8266. Он работает на частоте 240 МГц вместо 160 МГц, имеет два ядра и аппаратный FPU с одинарной точностью. Особенно, если вы хотите использовать Wi-Fi или что-то еще, что зависит от прерываний, вы можете выполнять DSP на втором ядре и не беспокоиться о том, что ваши вычисления будут отодвинуты на второй план тем, что происходит в вашем основном цикле., @J...
Да, я уже использовал esp32 раньше с этим чипом attitude. Я просто сделал каждый вар поплавком, и он поплыл дальше. Но это для дешевой игрушки, а дополнительные 2 доллара за esp32 - это слишком много., @mark-hahn