В настоящее время я пишу программу, в которой с учетом двух входных матриц (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;
}
}
Я не понимаю, какое отношение это имеет к моему вопросу? Не могли бы вы объяснить это немного лучше? Я хочу знать, является ли приведение типов каждого элемента массива по памяти таким же, как копирование всего массива?
Я почти уверен, что создание одного float
за раз будет повторно использовать одно и то же пространство для этого единственного значения. Нет особой причины «сохранить это на потом» и каждый раз использовать новое пространство в цикле.
@Elyon, я имею в виду, что вы выделяете память в объявлении, например. в int arr1[10]
вы выделяете 10 целых чисел (по sizeof(int)
байтов), а в float arr2[10]
вы выделяете 10 чисел с плавающей запятой (по sizeof(float)
байтам каждое). Тип, приведенный в sum += (float) ...
, использует только временно «некоторое» пространство (память стека или регистры), но это не то, что действительно имеет значение для потребления памяти вашим приложением.
Если вы скопировали массив input_a
в float copy_a[…];
, а затем выполнили вычисления, используя copy_a
вместо input_a
, вам придется преобразовать значения из int8_t
в float
только один раз. Используя написанный код, вы преобразуете элементы input_a
каждый раз, когда их используете. Существует компромисс между временем и пространством — и только у вас есть достаточно информации, чтобы сказать, принесет ли это однократное преобразование значительный выигрыш в производительности. Если пространство ограничено, то, что у вас есть, вполне подойдет. Если время больше беспокоит, вы можете попробовать использовать преобразованную копию и посмотреть, ускорит ли это операции.
Чтобы внести ясность, компилятор не будет выполнять за вас копирование с преобразованием — это выходит за рамки его компетенции. Но вы можете решиться на это.
Большинство компьютеров имеют небольшое количество выделенных областей памяти внутри процессора, называемых регистрами. Они используются для временной работы при выполнении вычислений, и обычно их всего несколько (примерно порядка 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
.
Пожалуйста, также покажите свои объявления
input_a
,input_b
,output
. Тогда, возможно, вы сами сможете ответить на этот вопрос.