Что быстрее на esp8266: 64-битная математика или математика с плавающей запятой?

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

Я надеюсь использовать формат с фиксированной запятой с 24 битами слева от десятичной запятой и 8 битами справа. Таким образом, число будет сохранено в int32_t как 0xffffff.ff. Типичной операцией было бы умножение типа ((i64) x * y) >> 24).

Итак, необходима 64-битная математика. Будет ли это значительно медленнее, чем умножение с плавающей запятой?

, 👍10

Обсуждение

выполните 10 000 операций каждого ... раза, когда результаты, @jsotola

Кстати, если я был отвергнут, потому что ответ казался очевидным, тогда они должны прочитать остальную часть этой темы., @mark-hahn

Если вам нужны быстрые операции с плавающей запятой с одинарной точностью, рассмотрите ESP32 вместо ESP8266. Он работает на частоте 240 МГц вместо 160 МГц, имеет два ядра и аппаратный FPU с одинарной точностью. Особенно, если вы хотите использовать Wi-Fi или что-то еще, что зависит от прерываний, вы можете выполнять DSP на втором ядре и не беспокоиться о том, что ваши вычисления будут отодвинуты на второй план тем, что происходит в вашем основном цикле., @J...

Да, я уже использовал esp32 раньше с этим чипом attitude. Я просто сделал каждый вар поплавком, и он поплыл дальше. Но это для дешевой игрушки, а дополнительные 2 доллара за esp32 - это слишком много., @mark-hahn


2 ответа


11

Вычисление относительной производительности 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


6

Хорошо, я взял на себя труд закодировать процедуры с фиксированной запятой и протестировать их. Вот этот код ...


// процедуры с фиксированной запятой для 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