Занимает ли приведение типов каждого элемента массива меньше места, чем копирование массива в новый?

В настоящее время я пишу программу, в которой с учетом двух входных матриц (int8_t и float соответственно) я вычисляю их умножение.

Из соображений памяти я не хочу, чтобы вся матрица int8 была преобразована в плавающий тип (или любой тип, занимающий в памяти более 8 бит). Я считаю, что в C при умножении int на float происходит неявное приведение типов, которое выполняется для преобразования int в float для выполнения операции.

Теперь мой вопрос: если я приведу свой ввод int8 как число с плавающей запятой, а затем выполню вычисления, что на самом деле произойдет в памяти? Перезаписывается ли это в других бесполезных пространствах памяти после завершения приведения или оно занимает дополнительное место, как если бы я создал массив с плавающей запятой, в который скопировал свои данные?

for (int i = 0; i < n; ++i) {
    for (int j = 0; j < m; ++j) {
        float sum = 0;
        for (int l = 0; l < k; ++l) {
            sum += (float) input_a[i*k + l] * input_b[l*m + j];
        }
        output[i*m + j] = sum;
    }
} 

Пожалуйста, также покажите свои объявления input_a, input_b, output. Тогда, возможно, вы сами сможете ответить на этот вопрос.

jhole 02.08.2024 15:45

Я не понимаю, какое отношение это имеет к моему вопросу? Не могли бы вы объяснить это немного лучше? Я хочу знать, является ли приведение типов каждого элемента массива по памяти таким же, как копирование всего массива?

Elyon 02.08.2024 16:03

Я почти уверен, что создание одного float за раз будет повторно использовать одно и то же пространство для этого единственного значения. Нет особой причины «сохранить это на потом» и каждый раз использовать новое пространство в цикле.

BoP 02.08.2024 16:07

@Elyon, я имею в виду, что вы выделяете память в объявлении, например. в int arr1[10] вы выделяете 10 целых чисел (по sizeof(int) байтов), а в float arr2[10] вы выделяете 10 чисел с плавающей запятой (по sizeof(float)байтам каждое). Тип, приведенный в sum += (float) ..., использует только временно «некоторое» пространство (память стека или регистры), но это не то, что действительно имеет значение для потребления памяти вашим приложением.

jhole 02.08.2024 16:23

Если вы скопировали массив input_a в float copy_a[…];, а затем выполнили вычисления, используя copy_a вместо input_a, вам придется преобразовать значения из int8_t в float только один раз. Используя написанный код, вы преобразуете элементы input_a каждый раз, когда их используете. Существует компромисс между временем и пространством — и только у вас есть достаточно информации, чтобы сказать, принесет ли это однократное преобразование значительный выигрыш в производительности. Если пространство ограничено, то, что у вас есть, вполне подойдет. Если время больше беспокоит, вы можете попробовать использовать преобразованную копию и посмотреть, ускорит ли это операции.

Jonathan Leffler 02.08.2024 17:23

Чтобы внести ясность, компилятор не будет выполнять за вас копирование с преобразованием — это выходит за рамки его компетенции. Но вы можете решиться на это.

Jonathan Leffler 02.08.2024 17:24
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
0
6
51
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Большинство компьютеров имеют небольшое количество выделенных областей памяти внутри процессора, называемых регистрами. Они используются для временной работы при выполнении вычислений, и обычно их всего несколько (примерно порядка 10–100): недостаточно места для хранения огромного массива, но определенно достаточно места для обработки отдельного элемента массива. Эта память не находится в оперативной памяти, а физически встроена в процессор.

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

Я говорю «обычно» и «обычно» выше, потому что это не обязательно произойдет. Структура вашего кода может быть такой, что в тот момент, когда они вам нужны, регистры могут отсутствовать. Оптимизатор внутри компилятора может увидеть, что вы пытаетесь сделать, и переписать ваш код каким-либо другим способом. Но на самом деле можно с уверенностью сказать, что вы получите здесь регистр, а если нет, то это будет очень небольшой объем оперативной памяти, который, вероятно, будет повторно использоваться от итерации к итерации.

С другой стороны, если вы создадите совершенно новый массив из n чисел с плавающей запятой и выполните все приведения типов одновременно, вы, скорее всего, в конечном итоге израсходуете n × sizeof(float) байт памяти, потому что вы явно запросили место для размещения н плавает. Память, которую вы получите обратно, вероятно, будет в оперативной памяти, потому что здесь слишком много чисел с плавающей запятой, чтобы каждому из них можно было дать регистр. Поэтому вы должны предположить, что этот подход потребует больше памяти. Если вы имеете дело с гигабайтами данных, такой подход может оказаться непомерно дорогим.

Я сказал здесь «вероятно» и «вероятно», потому что оптимизирующие компиляторы в наши дни становятся действительно хороши в том, что они делают, и вполне возможно, что компилятор увидит, что вам нужен массив только для целей цикла, и поэтому перепишет ваш код так, чтобы просто делайте приведения типов. Но, вероятно, не стоит на это полагаться.

Ответ принят как подходящий

sum += (float) input_a[i*k + l] * input_b[l*m + j]; указывает, какое вычисление необходимо выполнить. Там открыто говорится, что нужно извлечь элемент i*k + l из input_a, преобразовать его в float, извлечь элемент l*m + j из input_b, умножить их (включая неявное преобразование второго операнда в float), добавить их в sum и сохранить результат в sum.

Ничто в этом не говорит о необходимости сохранять что-либо в какой-либо памяти, кроме sum. Стандарт C позволяет компилятору реализовать эти вычисления любым способом, который не меняет наблюдаемое поведение программы, которое состоит из ее вывода, взаимодействия ввода-вывода и доступа к изменчивым объектам. В большинстве компиляторов и большинства процессоров компилятор генерирует код для выполнения этой операции полностью в регистрах процессора:

  • Индексы будут рассчитываться в регистрах процессора.
  • Элементы массива будут загружены в регистры процессора.
  • Значения будут преобразованы в float в регистрах процессора.
  • Умножение и сложение будут выполняться в регистрах процессора.
  • Результат будет либо сохранен в sum в памяти, либо оптимизация компилятора сохранит sum в регистрах процессора до тех пор, пока не будет выполнено окончательное output[i*m + j] = sum;.

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

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

Ни в одной обычной реализации C компилятор не генерирует целый массив элементов float для хранения различных значений, которые этот код преобразует в float.

Другие вопросы по теме