Если то – указатель, и это – указатель, да ещё и на один и тот же тип, то у них размеры должны быть одинаковые, я из этого исходил.
Как одинаковость или разность значений, возвращаемых sizeof, определяет тип переменной?
Не в эту сторону, а в обратную.
Не типы одинаковые, потому что sizeof одинаковый, а – раз одинаковые типы, то и sizeof у них должен быть одинаковый.
Вы вообще в курсе, что такое типы?
Конечно, это же основополагающее начало языка C.
Справочник, кстати, был отличный, перевод канонического стандарта, кажется, K&R, но я не уверен. Чай, не википедия какая. И про типы там всё было хорошо написано.
Возможно, и отличный, правда, из ваших слов следует, что, похоже, местами, неправильный.
Все ваши примеры отлично работают в пределах одного исходинка.
Согласен, в пределах одного работают.
Попробуйте передать ссылку на ваш "массив массивов" во внешнюю процедуру, определенную в прилинкованной библиотеке и посмотрите, как там будет осуществляться доступ.
Обязательно попробую, прямо сейчас и попробую, только функцию размещу вместо библиотеки в отдельно компилирующемся файле.
Ведь это не меняет сути?
Hint: подумайте о том, как та процедура узнает о размерности массива, по одному указателю.
Я прямо расскажу об этом этой самой процедуре путём описания соответствующего типа её параметра.
Ну и про типы почитайте что-нибудь, очень полезно, рекомендую.
Лучше я тогда сразу и продемонстрирую своё понимание типов, чтобы у вас не оставалось сомнений, что я про типы не только читал, но и как следует в них разобрался.
Функция, в числе прочего, будет принимать константный указатель на "4-мерный массив".
На самом деле, это будет константный указатель на массив массивов массивов массивов int'ов.
В функции fun заводится "4-мерный" массив, по 10 элементов в каждом "измерении", и инициализируется 0-ми.
Далее, сначала распечатывается значение из массива по индексам 1, 2, 3 и 4 с использованием функции get_array_element, скомпилированной в другом файле, затем это же место модифицируется путём прямого обращения к массиву, затем опять читается функцией get_array_element, чтобы убедиться, что функция "видит" массив, и видит его правильно.
После этого распечатывается значение из массива по индексам 4, 3, 2 и 1 с помощью прямого обращения к массиву, затем это же место модифицируется функцией set_array_element, и опять распечатывается с помощью прямого обращения к массиву, чтобы увидеть, что функция записала значение в массив, и записала его туда, куда нужно.
Реализации функций get_array_element и set_array_element.
В числе прочего, функции принимают указатель на массив.
Видите, как я передал информацию о размерностях массива по одному указателю?
Файл array.h:
#ifndef ARRAY_H__
#define ARRAY_H__
#include <stddef.h>
int get_array_element(size_t x0, size_t x1, size_t x2, size_t x3, int (*array)[10][10][10][10]);
void set_array_element(size_t x0, size_t x1, size_t x2, size_t x3, int (*array)[10][10][10][10], int value);
#endif // ARRAY_H__
Здесь – декларации функций get_array_element и set_array_element, чтобы из main.c их можно было правильно вызвать.
Поскольку godbolt теперь поддерживает cmake с возможностью компиляции множества файлов, предоставляя примитивную IDE, то – вот ссылка на микро-проект с этим кодом.
Теперь самое интересное: в ассемблерном коде для функции main видно, что функция fun вызывается, а не inline'ится:
main:
sub rsp,0x8
mov edi,0x309
call 401170 <fun>
xor eax,eax
add rsp,0x8
ret
В ассемблерном коде функции fun также видно, что функции get_array_element и set_array_element вызываются, а не inline'ятся (компилятору здесь некуда деться, потому что компиляция – раздельная):
Соглашусь с вами в том, что вы убедительно показали, что данный пример ничего не доказывает по сути обсуждения.
Это потому, что вы не связываете адресную арифметику с размером элемента.
Что касается адресной арифметики, то ей наплевать и на размер элемента тоже. Ей важны собственно адреса элементов.
Размер элемента является фундаментальнейшей и определяющей сущностью для адресной арифметики, для неё нет ничего важнее.
struct S {
int i;
char c;
};
struct S *
fun(struct S *p) {
return p + 1;
}
Если посмотреть, во что транслируется fun, то мы увидим следующее (ссылка на godbolt):
fun:
lea rax, [rdi+8]
ret
Откуда компилятор знает, что надо на 8 "шагнуть"? Он совсем не знает, откуда вообще взялся этот указатель: на элемент массива он изначально указывает, на поле структуры или вообще, на отдельную переменную.
Более того, могут быть различные вызовы этой, одной и той же, функции с указателями на различные, в смысле предыдущего абзаца, объекты, а обработка-то должна быть какой-то одной: указатель не несёт в себе информации о том, в составе чего находится объект.
Поэтому на что адресной арифметике точно плевать, так это – на происхождение указателя. Но ей точно не плевать на размер типа данных, на который указывает указатель. Именно потому, что размер struct S составляет 8 байт, и только поэтому, численное значение указателя увеличивается на 8. В состав чего входит объект, а также то, как он устроен внутри, имеют для адресной арифметики строго нулевое значение. Для неё важен исключительно размер элемента и только он.
Если раскомментировать закомментированный код, то, несмотря на то, что функции fun0 и fun1 станут идентичными, сразу же возникнет ошибка:
<source>: In function 'fun0':
<source>:9:43: error: invalid application of 'sizeof' to incomplete type 'struct S'
9 | printf("sizeof *p: %zu\n", sizeof *p);
| ^
<source>:10:10: error: increment of pointer to an incomplete type 'struct S'
10 | p++;
| ^~
Во время определения функции fun0 структура struct S ещё не определена, она только объявлена, а это значит, кроме прочего, что неизвестен её размер.
Именно потому, что адресная арифметика не существует без размера элемента, ругань относится не только к операции sizeof, но и к операции инкремента указателя.
Но как только структура определена, и становится известен её размер, у компилятора, при компиляции функции fun1, претензии пропадают не только к операции sizeof, но и к инкременту указателя.
А что изменилось-то? Стал известен размер struct S. И этого оказалось совершенно достаточно.
То есть:
Для работы адресной арифметики строго необходим размер типа, на который указывает указатель.
Кроме размера типа, адресной арифметике больше ничего не нужно, будь то происхождение объекта или его внутренняя структура, – это никак не влияет на адресную арифметику.
Программы не эквивалентны. Вы в Си перешли для использования в массиве от типа char к int, а в Фортране оставили integer1. Поменяйте integer1 на integer, и получите длинный конвейер (для моего процессора конвейер имеет длину 16 байт).
Я Fortran'а не знаю, особенно современного, поэтому не догадался, что значит эта 1. В программе на C я этот тип за' typedef'ил, и, получается, совсем не зря.
Заметьте, все компиляторы единодушны. Размеры указателя и массива x отличаются.
Это потому, что массив есть "смежно выделенное непустое множество объектов с определенным типом объекта-члена, называемым типом элемента", а никакой не указатель.
10 смежных char' ов – вот вам и sizeof, равный 10.
Пойдем далее, какой тип у x в этом коде:
char x[10][10];
Возьму на себя смелость утверждать, что это будет char**, то есть указатель на массив указателей.
Если применить определение из стандарта, то никакой это не указатель на указатель.
Лучше взять char x[10][20];, чтобы проще отличать было.
x – это 10 смежно выделенных элементов, каждый из которых есть char y[20];. А y – (условный, конечно, y) – это 20 смежно выделенных элементов, каждый из которых есть char.
То, что существует неявное преобразование от массива к указателю на его первый элемент, не делает массив указателем.
Фактически, код:
char c=x[3][3];
эквивалентен коду:
char c=*(*(x+3)+3);
Да, эквивалентно, и поэтому можно написать так:
char c = 3[3[x]];
И ни один компилятор даже не пикнет (я не шучу).
Но это тоже никоим образом не превращает массив в указатель на его первый элемент.
То, что компилятор делает оптимизацию, превращая массив в фортрановский (да, массивы такого типа так называются), нарушает стандарты.
Вот странно.
Вроде и место в стандарте показал, где массив определяется, а до сих пор слышу, про какое-то нарушение.
Если я в другом месте (а лучше всего, в другом объектном файле или библиотеке) попытаюсь дважды разыменовать указатель char **x, то получу в лучшем случае SIGSEGV, в худшем -- рандомное значение.
Смотрите, а внаглую 10-кратно разыменовал 10-мерный массив (ссылка godbolt):
и ничего не упало ни в каком из компиляторов. Никаких, понимаете ли, SIGSEGV:
sizeof x: 3628800
**********x: !
Оптимизацию специально отключил, чтобы в ассемблерном коде видно было, что в массив кладётся 33 ('!') при инициализации и потом читается при разыменовании, а также распечатал размер массива, чтобы было видно, что он и правда, 10-мерный.
Да, интересный способ вычислить факториал 10...
Но функция, получившая этот указатель, будет обязана разыменовывать указатели как положено, делая по нескольку лишних запросов в память, с этим ничего не поделаешь.
Массив – не указатель.
Видите, я следую стандарту, и у меня ничего не падает. И никаких лишних обращений к памяти у меня нет.
Неужели вы продолжите утверждать, что массив массивов - это массив указателей?
Преимущества в плане качества кода у компиляторов Фортрана перед C нет, как, впрочем, нет и особого отставания.
Вот бы ещё и в статье бы было так сказано...
За счёт этого программа, написанная неопытным программистом на Фортране будет гораздо производительнее программы, написанной неопытным программистом на C++. Какой-нибудь физик перепишет алгоритм на Фортран почти 1-в-1 из статьи и получит результат за приемлемое время, не имея при этом вообще никакого представления о том, как матрицы размещены в памяти и какие инструкции выполняет процессор при их умножении.
Вот в это уже вполне верится.
К тому же, C, и особенно C++, – не для использования новичками.
20 Any number of derived types can be constructed from the object and function types, as follows: — An array type describes a contiguously allocated nonempty set of objects with a particular member object type, called the element type. The element type shall be complete whenever the array type is specified. Array types are characterized by their element type and by the number of elements in the array. An array type is said to be derived from its element type, and if its element type is T, the array type is sometimes called ‘‘array of T’’. The construction of an array type from an element type is called ‘‘array type derivation’’.
Всё.
Где здесь сказано, что массивы бывают многомерные, и, тем более, что тогда они должны быть массивами ссылок?
В C нет многомерных массивов. Зато там есть массивы массивов. Это можно продолжать рекурсивно.
У меня в изначальном примере не 5-мерный массив, а "5-мерный". Это значит, что, на самом деле, там массив массивов массивов массивов массивов int'ов.
Перегрузка операторов в Фортране есть, ООП есть, по сравнению с C++ только шаблонов нет. Но можно писать код, общий относительно вариантов типа (real(4), real(8), real(16)). Небольшое расширение вполне себе реализуется.
Получается, это немного лучше, чем C-шный _Generic, но явно слабее чем C++. Но если этого хватает, то – и ладно.
В статье утверждалось, что Fortran значительно быстрее C/C++, но увидеть этого мне пока не удалось. По эффективности я не вижу преимуществ Fortran'а перед C.
А теперь вернёмся к тому, о чём я писал с самого начала – к обработке двухмерного массива. Подставим в сложение c[n][m] вместо c[m][n]. И то же самое в фортране – вместо простого сложения массивов напишем цикл (наружный цикл по j, так как в фортране массивы хранятся по столбцам)
program main
integer*1, allocatable :: a (:,:), b (:,:), c (:,:)
integer :: m, n, mm, nn, i, j
real :: u
call random_init (.true., .true.)
call random_number (u)
m = floor (u * 102)
call random_number (u)
n = floor (u * 205)
allocate (a (m,n), b (m, n), c (n, m))
do j=1, N
do i=1, M
call random_number (u)
b (i, j) = floor (u * 100)
end do
end do
nn = 3
c = nn
do j=1, N
do i=1, M
a (i,j) = b (i,j) + c (j,i)
end do
end do
print *, m, n
print *, a
end program main
Программа слегка подрихтована, чтобы M и N всегда были одинаковыми и равными таковым в последующем коде на C, но чтобы выглядели как настоящие достаточно случайные.
gfortran:
.L17:
movzx eax, BYTE PTR [rdx+r11]
movzx r8d, BYTE PTR [rdx+rsi*2]
vmovd xmm0, DWORD PTR [r12+rcx]
sal eax, 8
or eax, r8d
movzx r8d, BYTE PTR [rdx+rsi]
sal eax, 8
or eax, r8d
movzx r8d, BYTE PTR [rdx]
add rdx, rbp
sal eax, 8
or eax, r8d
vmovd xmm4, eax
vpaddb xmm0, xmm4, xmm0
vmovd DWORD PTR [r10+rcx], xmm0
add rcx, 4
cmp rbx, rcx
jne .L17
Уже известная тройка инструкций и затем add rcx, 4.
В сообщении компилятора написано:
app/example.f90:28:7: optimized: loop vectorized using 4 byte vectors
И в этом отрывке, если проследить, то видно, что обращения, действительно, 4-х байтовые (DWORD PTR).
Это – не векторизация, компилятора только вид сделал, что векторизовал.
Зато здесь шаг, как видно из инструкции addr 10, 32 , – 32, и обращения – 32-байтовые (YMMWORD PTR).
Да, это уже – векторизация, причём, довольно мощная.
Ассемблерный код в фортране не сильно изменился, там такая же команда paddb / vpaddb в чуть более сложном окружении. А сишный кодогенератор пошёл вразнос (я уже писал, что не могу проинтерпретировать увиденное).
Как видно, gfortran не справился, 4-байтный "вектор" – это не векторизация.
Теперь посмотрим C-шный код, максимально близкий к рассматриваемому Frotran'овскому (ссылка на godbolt):
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <time.h>
#define ELEMS(a) (sizeof (a) / sizeof *(a))
typedef int type;
void fun(size_t M, size_t N) {
type (*a)[M][N] = malloc(sizeof *a);
if (a) {
type (*b)[M][N] = malloc(sizeof *b);
if (b) {
type (*c)[N][M] = malloc(sizeof *c);
if (c) {
for (size_t i = 0; i < ELEMS(*b); ++i) {
for (size_t j = 0; j < ELEMS((*b)[i]); ++j) {
(*b)[i][j] = rand();
}
}
for (size_t i = 0; i < ELEMS(*c); ++i) {
for (size_t j = 0; j < ELEMS((*c)[i]); ++j) {
(*c)[i][j] = 3;
}
}
for (size_t i = 0; i < ELEMS(*a); ++i) {
for (size_t j = 0; j < ELEMS((*a)[i]); ++j) {
(*a)[i][j] = (*b)[i][j] + (*c)[j][i];
}
}
printf("a[0][0]: %i\n", (*a)[0][0]);
free(c);
}
free(b);
}
free(a);
}
}
int main(void) {
//srand(time(NULL));
srand(0);
size_t M = 100. * rand() / RAND_MAX;
size_t N = 100. * rand() / RAND_MAX;
printf("M: %zu, N: %zu\n", M, N);
fun(M, N);
return EXIT_SUCCESS;
}
Смотрите, — все компиляторы так делают, у всех одно-единственное обращение к памяти.
Может, всё-таки, дело не в нарушении стандарта?
Вообще-то, сообщество настолько огромно, и у него такое количество глубоко разбирающихся в вопросе людей, что подобное "нарушение" было бы вскрыто в короткие сроки и давно исправлено.
В C нет многомерных массивов, зато есть массивы массивов.
Видно, что сумма размеров полей структуры на 3 байта меньше размера структуры. В структуре – дыра, причём, с краю, а не в середине, но она принадлежит структуре, входит в неё, является её неотъемлемой частью. Именно поэтому размер структуры равен 8.
Массив структур в качестве элементов имеет объекты типа struct S размером 8 байт. Количество элементов – 3. Размер массива – 24.
Между элементами массива, каждый из которых имеет размер 8 байт, нет никаких дыр. Они есть внутри каждого элемента, но это – внутреннее свойство типа элемента, а не свойство самого массива.
Адресная арифметика "щёлкает" на размер элемента и абстрагируется от его внутренних свойств. Поэтому ей все равно, есть дыры в каждом из элементов, нет, сколько их и какого они размера.
Обратите внимание, что contiguously allocated относится к objects, а element type – к member object. Отсюда никак не следует, что тип элемента, т.е. тип объекта-члена – это то же самое, что непрерывно размещённые в памяти объекты. Буквально: размещённый набор объектов с определёнными типами объектов-членов.
Здесь имеется ввиду, что тип у всех объектов должен быть одинаковый. И объекты не просто размещённые, а смежно или непрерывно размещённые.
Переводчик №1 (ссылка): "Тип массива описывает смежно выделенный непустой набор объектов с определенным типом объекта-члена, называемым типом элемента".
Переводчик №2 (ссылка): "Тип массива описывает непрерывно выделенный непустой набор объектов с определенным типом объекта-члена, называемым типом элемента".
"Смежно/непрерывно выделенный непустой набор объектов".
В отличие от структур, о которых можно прочесть в следующем предложении стандарта:
— A structure type describes a sequentially allocated nonempty set of member objects (and, in certain ircumstances, an incomplete array), each of which has an optionally specified name and possibly distinct type.
Переводчик №1 (ссылка): "Тип структуры описывает последовательно выделенный непустой набор объектов-членов (и, при определенных обстоятельствах, неполный массив), каждый из которых имеет опционально заданное имя и, возможно, отдельный тип".
Переводчик №2 (ссылка): "Тип структуры описывает последовательно размещенный непустой набор объектов-членов (и, в некоторых случаях, неполный массив), каждый из которых имеет необязательно указанное имя и, возможно, отдельный тип".
Для массивов размещение смежное/непрерывное, а для структур – лишь последовательное (упорядоченное по порядку объявления полей) без требования непрерывности размещения.
И тип элементов – для массивов один и тот же, для структур – нет.
Да и по сути, машин с адресацией памяти словами – вагон и маленькая тележка.
Вы уже приводили пример PDP-11, и выяснилось, что никакой проблемы нет, ибо наличествует и адресация байтами, и в этом случае доступны и нечётные адреса.
В том, что на практике бывают массивы с дырками между элементами, я совершенно уверен, хотя лично с языком Си на таких машинах не работал.
Чья-либо уверенность в данном случае ничего не проясняет. Тем более, если человек не работал с языком C.
Более интересный вопрос другой: бывают ли в Си многомерные массивы с дополнительными дырками между строками?
Нет, не бывают.
В принципе, там и многомерных не бывает, бывают массивы массивов.
Самое вопиющее различие -- многомерные массивы. В фортране для доступа к произвольному элементу требуется одно обращение к памяти (чаще всего). В Си -- столько, сколько измерений у массива.
"5-мерный" массив (передаётся указатель на массив):
int fun(size_t i, size_t j, size_t k, size_t l, size_t m, int (*a)[5][6][7][8][9]) {
return (*a)[i][j][k][l][m];
}
Сколько обращений к памяти? Я вижу одно, в предпоследней инструкции.
На эти грабли, как правило, наступает каждый аспирант, загоревшийся идеей переписать код с фортрана на Си и потом недоумевающий, почему его версия работает в несколько раз медленнее)
Наверное, у того аспиранта массивы неправильные.
Грабли как правило заключаются в отсутствии владении языком, неэффективным его использованием, а не недостатком языка.
Посмотрел, кстати – gfortran это сложение массивов транслирует в ОДНУ векторную команду paddb.
Нет, он транслирует сложение массивов ровно в те же векторные инструкции (в количестве 3-х), в которые транслируется и та программа на C, которую я приводил:
Ссылка на godbolt с вашим кодом, я оттуда достал этот фрагмент из ассемблерного листинга для компилятора gfortran (добавил опцию -march=native, чтобы более эффективные векторные инструкции использовались).
Однако, и flang, и ifort (в последнем надо как следует поискать нужное место), выдают для сложения матриц ровно такой же код с точностью до конкретных используемых регистров.
Видите, выясняется, что Fortran не имеет преимуществ в производительности перед C.
Вы не забывайте, что всеми этими макросами, sizeof'ами, циклами, преобразованиями пойнтеров туда и обратно вы расписываете аналог следующего фортрановского кода
И что? Язык C – общего назначения, в него не тащат, что не попадя.
Если перейти на C++, Fortran проиграет, ибо C++ позволяет создать себе инструмент, причём такой, какой надо, и пользоваться им.
В Fortran'е же, насколько я понимаю, есть только то, что есть.
6.2.5 Types ... 20 Any number of derived types can be constructed from the object and function types, as follows: — An array type describes a contiguously allocated nonempty set of objects with a particular member object type, called the element type. ...
Чтобы была работоспособной адресная арифметика, достаточно того, чтобы инкремент указателя на элемент массива давал следующий элемент.
Тогда все массивы, а также области памяти, интерпретируемые как массивы, будут "дырявыми".
Но в C они не могут быть "дырявыми".
Как вы вообще представляете себе char [] на машине с невозможностью обращения по нечётному адресу (как, например, некоторые модели PDP, хорошо известные разработчикам Си)?
Невозможность обратиться по нечетному адресу характерна лишь для регистров R6 (PC) и R7 (SP). Для остальных регистров проблемы обратиться к байту, в том числе, по нечётному адресу, нет.
Очень просто представляю: байтовые инструкции.
В близнеце Си Паскале – так там специально даже есть синтаксис packed array.
Хорошо, что Вы тратите своё время и пытаетесь доказательно разобраться в вопросе
А как ещё иначе? Авторитетов же не существует.
К тому же, чтобы навыки не "загнивали", их необходимо тренировать, а на это так и так уходит время.
но плохо, что при этом игнорируете часть смысла сказанного.
Это, видимо, потому, что я возражал по конкретному пункту о необходимости явно прописывать адресную арифметику, а потом по конкретному другому пункту касательно "устройства" массива массивов.
Когда вы в своей функции add обходите массив по строкам, то вы никак не используете его двухмерность. Фактически это проход по одномерному массиву, расписанный в два индекса, и компилятор понимает, что происходит обращение к последовательным ячейкам памяти.
Ну, то есть, не требуется прописывать адресную арифметику, и компилятор может соптимизировать, поскольку "понимает", с чем работает.
Если вы заметите, я изначально писал про перемножение матриц, потому что там таким трюком не обойтись.
Во-первых, это никакой не трюк, а штатная возможность, абсолютно переносимая и обязанная одинаковым образом быть поддержанной всеми компиляторами, претендующими на соблюдение стандарта. А, во-вторых, основной фоновый контекст у нас здесь – преимущество (или отсутствие такового) Fortran'а перед C в рассматриваемой части.
Если вы в своей функции add переставите местами индексы, например, у массива c (т.е. c[n,m] вместо c[m,n]), то компилятор начинает выдавать уже такую хтонь, которую я затрудняюсь проинтерпретировать.
Вы хотите сказать, что компилятор Fortran'а аналогичный код в тех же условиях сумеет векторизовать?
Далее, вы же сами должны хорошо понимать, что ваш макрос ELEMS работает только по совпадению из-за невыровненности элементов массива на границу, большую их длины.
Макрос ELEMS работает для любых случаев.
Элементы массива (и сам массив, а также массив массивов и далее рекурсивно) выровнены согласно требованию к выравниванию для типа элемента массива. Между элементами массива нет "пропусков", они расположены "вплотную" друг к другу, и sizeof массива строго равен количеству его элементов, умноженному на sizeof его элемента.
Такое поведение не гарантируется языком Си (уже на той же самой PDP-11 нечётные адреса зачастую были запрещены).
Расположение элементов массива "влотную", без "пропусков" гарантируется стандартом, и это необходимое условие для того, чтобы адресная арифметика была в принципе работоспособной.
Макрос ELEMS прямо опирается на гарантии стандарта и именно поэтому работает для любых случаев.
В структурах между полями могут быть пропуски, но там и типы у полей могут быть разными, и, соответственно, у них могут быть разные размеры и требования по выравниванию, что и может приводить к подобным эффектам.
Конечно, никакого принципиального значения этот макрос не имеет, можно перейти просто к M и N, но это ещё затруднит работу векторизатора.
Нет необходимости переходить к M и N, отказываясь от данного макроса по надуманной причине, поэтому у "векторизатора" затруднений, связанных с этим, не появится.
В том числе на некоторых векторных архитектурах размеры самих векторов должны выравниваться на длину векторных регистров.
Язык C предоставляет средства для реализации повышенных требований к выравниванию, поэтому здесь также нет никаких проблем.
По адресу видно, что a0 реально выровнен на границу 16, а a1 – уже на границу 64. Явный код для выравнивания на 64 для массива a1 можно также увидеть в ассемблере у каждого из компиляторов.
Что касается массива указателей, то я был неправ, высказав своё утверждение в таком виде, оно неверно. В целом я думал о том, что написано ниже, и неудачно выразил свою мысль.
Но доказательств для этого потребовалось больше, чем, я думал, будет достаточно.
Теперь, о практике программирования в целом. Думаю, вы согласитесь, что по целому ряду причин мало кто в реальной жизни будет писать функции так, как это сделано в вашем демонстрационном коде.
Я не могу судить о реальной жизни в широком смысле, ибо недостаточно информации, поэтому не могу согласиться или не согласиться.
Да, junior'ы не смогут такое написать, и даже, наверное, часть middle'ов не сможет, особенно вариант с VLA, но достаточно им по-настоящему объяснить, как устроены типы в языке, и на чём базируется адресная арифметика, а не как это часто делают современные преподаватели, и, после некоторого времени, потраченного на практику, такое даже junior'ы смогут, здесь нет чего-то заумного.
Программист на С и особенно С++ будет выбирать решение, более соответсвующее принципу инкапсуляции и более совместимое со стандартной библиотекой.
Использование указателя на массив никак не ограничивает инкапсуляцию, а у программиста на C++, кроме указателей, ещё наличествуют в активе и ссылки, он может использовать ссылку на массив.
Поскольку речь идёт об инкапсуляции, то реализация может быть любой.
Если кто-то ничего не знает, кроме как о std::vector, (как ещё понять "более совместимое со стандартной библиотекой", но при этом не дающее возможности применить указатель на массив?) это не повод огульно обобщать на большинство (как ещё понять "программист на С и особенно С++ будет выбирать"?).
К тому же, сейчас активно используется такая процедура как review, что нивелирует огрехи, которые могут допустить не слишком искушённые программисты.
А это значит, что все эти трюки с массивами в С крайне малоприменимы, на практике там будет другая структура данных.
Повторюсь, это никакой не трюк, это совершенно штатная и абсолютно переносимая возможность языка.
Там будет та структура данных, которую выберет программист.
Если программист слабо владеет языком, он легко может выбрать неподходящее решение. Для этого обычно в компаниях бывают программисты, кототрые хорошо владеют используемым языком, и которые на review укажут, как следует изменить решение, чтобы оно стало эффективным.
И уже после этого там будет та структура данных, которую выберет грамотный программист, и которая эффективна для используемого языка.
В отличие от Фортрана, где податься некуда, и сложные структуры данных, как правило, представляются в виде набора массивов.
То есть, получается, что в этом месте C имеет преимущество перед Fortran'ом, поскольку даёт больше возможностей и более гибок.
Наконец, как тут уже отмечалось в комментариях выше, не будем забывать, что фортрановскую программу можно простой перекомпиляцией перевести с SSE на CUDA.
Выходит, это единственное преимущество Fortran'а в данной части.
Верно, это же C, и вы сами выбрали случай, когда размерности становятся известны только в run-time'е.
Их в любом случае необходимо передавать, только в других языках это происходит "под капотом", и отменить это нельзя, а здесь, хоть и требуется выполнять явную передачу, но, зато этим можно управлять (передавать или не передавать, если не требуется).
и адресной арифметике полувручную
Где здесь, особенно в последнем варианте, адресная арифметика? (1)
Что значит, "полувручную"? (2)
А когда вы ещё учтёте возможность выравнивания, будет совсем вручную.
Функции malloc/calloc/realloc возвращают адрес с выравниванием, достаточным для хранения любого стандартного объекта.
Если же передаётся адрес настоящего массива, то он, по определению, выровнен.
Зачем вы пытаетесь найти изъяны там, где их нет?
Причём компилятор-то не знает, что значение M - это длина строки массива, и не может им воспользоваться при загрузке вектора в векторный процессор.
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#define K 1000
#define ELEMS(a) (sizeof (a) / sizeof *(a))
typedef int type;
void add(size_t M, size_t N, type (*a)[M][N], type (*b)[M][N], type (*c)[M][N]) {
for (size_t m = 0; m < ELEMS(*a); ++m) {
for (size_t n = 0; n < ELEMS((*a)[m]); ++n) {
(*a)[m][n] = (*b)[m][n] + (*c)[m][n];
}
}
}
void fill(size_t M, size_t N, type (*x)[M][N], type y) {
for (size_t i = 0; i < ELEMS(*x); ++i) {
for (size_t j = 0; j < ELEMS((*x)[i]); ++j) {
(*x)[i][j] = y;
}
}
}
int main(int argc, char **argv) {
(void)argv;
int m = argc + argc;
int n = m + argc * argc;
size_t M = m * K;
size_t N = n * K;
type (*a)[M][N] = malloc(sizeof *a);
if (a) {
type (*b)[M][N] = malloc(sizeof *b);
if (b) {
fill(M, N, b, m);
type (*c)[M][N] = malloc(sizeof *c);
if (c) {
fill(M, N, c, n);
add(M, N, a, b, c);
printf("a[0][0]: %i\n", (*a)[0][0]);
free(c);
}
free(b);
}
free(a);
}
return EXIT_SUCCESS;
}
и посмотрим, векторизуется ли он в функциях add и fill.
Функция fill заполняет массив одним и тем же значением.
Функция add складывает поэлементно два массива и поэлементно же кладёт результат в третий.
В данном коде во все элементы массива b кладётся 2, во все элементы массива c кладётся 3, затем они суммируются, а результат кладётся в массив a.
Первый элемент массива a распечатывается, чтобы icc не "думал", что, раз массив a после заполнения никак не используется, то можно ничего и не вычислять, и не выбрасывал вызов функции add.
В функции add видим в ассемблерном коде gcc следующее:
А здесь видите векторизацию? Компилятор опять смог.
Для clang'а и icc от Intel'а всё аналогично.
Если прогнать perf'ом, то получается следующее (у меня локально установлена другая версия gcc, поэтому используемые регистры в ассемблерном коде слегка другие):
Видно, что эти инструкции на самом деле работают, а не просто "валяются в коде рядом".
Это означает, что компилятор смог. Более того, смогли все опробованные мной компиляторы.
Вы можете ответить на вопросы, помеченные мной как (1) и (2)?
Итак:
В C массив массивов понимается как массив массивов, причём независимо от того, какой это массив, обычный или VLA.
В этом стиле доступна работа и с динамически выделенной памятью, причём даже в этом случае размерности каждого массива не обязаны быть константами времени компиляции.
Никакой дополнительной адресной арифметики при этом не требуется.
Никаких прочих накладных расходов, связанных с использованием указателей на void, нет.
Передача размерностей массива в рассматриваемом случае не зависит от языка и будет присутствовать в том или ином виде в любом языке.
Не существует никаких проблем с выравниванием, специфичных для применяемой техники.
При этом все широко используемые современные компиляторы прекрасно векторизуют код.
В этом смысле Fortran не имеет преимуществ перед C.
Если ещё учесть, что strict aliasing'ом можно управлять с помощью ключевого слова restrict, то станет очевидно, что в этом смысле Fortran вообще не имеет преимуществ перед C.
Напоминаю, что первый пункт в списке выше есть прямое возражение на ваше утверждение:
В Си массив массивов понимается как массив указателей на массивы, а не как двухмерный массив.
Переменными сделайте M и N, а не подставляйте вместо них препроцессором константы 2 и 3.
Да, давайте ещё и константность везде поубираем, чтобы она вас не смущала.
И чтобы компилятор не смог ничего лишнего соптимизировать, запутаем способ получения M и N, да так, чтобы в выражениях для их вычисления ещё и ни единой константы не было.
Вариант с VLA:
#define ELEMS(a) (sizeof (a) / sizeof *(a))
void fun(size_t M, size_t N) {
char a[M][N];
memset(&a, 0, sizeof a);
printf("sizeof a: %zu\n", sizeof a);
printf("&a: %p\n", (void *)&a);
for (size_t m = 0; m < ELEMS(a); ++ m) {
printf("\tsizeof a[%zu]: %zu\n", m, sizeof a[m]);
printf("\t&a[%zu]: %p\n", m, (void *)&a[m]);
for (size_t n = 0; n < ELEMS(a[m]); ++ n) {
printf("\t\t&a[%zu][%zu]: %p\n", m, n, (void *)&a[m][n]);
}
}
}
int main(int argc, char **argv) {
(void)argv;
size_t M = argc + argc;
size_t N = argc * (argc + argc) + argc;
fun(M, N);
return EXIT_SUCCESS;
}
Это я знаю, что программа будет запускаться без параметров, и argc будет равен 1.
Но компилятор из этого не может исходить, тем более, что я же могу второй раз ту же уже скомпилированную программу и с параметром запустить.
В C массив массивов понимается как массив массивов, причём независимо от того, какой это массив, обычный или VLA.
Более того, в этом стиле доступна работа и с динамически выделенной памятью, причём даже в этом случае размерности каждого массива не обязаны быть константами времени компиляции.
В этом смысле Fortran не имеет преимуществ перед C.
Если то – указатель, и это – указатель, да ещё и на один и тот же тип, то у них размеры должны быть одинаковые, я из этого исходил.
Не в эту сторону, а в обратную.
Не типы одинаковые, потому что
sizeof
одинаковый, а – раз одинаковые типы, то иsizeof
у них должен быть одинаковый.Конечно, это же основополагающее начало языка C.
Возможно, и отличный, правда, из ваших слов следует, что, похоже, местами, неправильный.
Согласен, в пределах одного работают.
Обязательно попробую, прямо сейчас и попробую, только функцию размещу вместо библиотеки в отдельно компилирующемся файле.
Ведь это не меняет сути?
Я прямо расскажу об этом этой самой процедуре путём описания соответствующего типа её параметра.
Лучше я тогда сразу и продемонстрирую своё понимание типов, чтобы у вас не оставалось сомнений, что я про типы не только читал, но и как следует в них разобрался.
Функция, в числе прочего, будет принимать константный указатель на "4-мерный массив".
На самом деле, это будет константный указатель на массив массивов массивов массивов
int
'ов.Две функции: одна читает, другая пишет.
Файл
main.c
:В функции
fun
заводится "4-мерный" массив, по 10 элементов в каждом "измерении", и инициализируется 0-ми.Далее, сначала распечатывается значение из массива по индексам 1, 2, 3 и 4 с использованием функции
get_array_element
, скомпилированной в другом файле, затем это же место модифицируется путём прямого обращения к массиву, затем опять читается функциейget_array_element
, чтобы убедиться, что функция "видит" массив, и видит его правильно.После этого распечатывается значение из массива по индексам 4, 3, 2 и 1 с помощью прямого обращения к массиву, затем это же место модифицируется функцией
set_array_element
, и опять распечатывается с помощью прямого обращения к массиву, чтобы увидеть, что функция записала значение в массив, и записала его туда, куда нужно.Файл
array.c
:Реализации функций
get_array_element
иset_array_element
.В числе прочего, функции принимают указатель на массив.
Видите, как я передал информацию о размерностях массива по одному указателю?
Файл
array.h
:Здесь – декларации функций
get_array_element
иset_array_element
, чтобы изmain.c
их можно было правильно вызвать.Поскольку godbolt теперь поддерживает cmake с возможностью компиляции множества файлов, предоставляя примитивную IDE, то – вот ссылка на микро-проект с этим кодом.
Теперь самое интересное: в ассемблерном коде для функции
main
видно, что функцияfun
вызывается, а не inline'ится:В ассемблерном коде функции
fun
также видно, что функцииget_array_element
иset_array_element
вызываются, а не inline'ятся (компилятору здесь некуда деться, потому что компиляция – раздельная):Долгожданный ассемблерный код функции
get_array_element
:Видите, сколько там обращений к памяти?
Я вижу только одно, в предпоследней инструкции.
Долгожданный ассемблерный код функции
set_array_element
:Видите, сколько в этой функции обращений к памяти?
Я опять вижу только одно, в предпоследней инструкции.
А, да, результат исполнения программы:
Я нигде не ошибся в рассуждениях и в предоставленном коде?
Это потому, что вы не связываете адресную арифметику с размером элемента.
Размер элемента является фундаментальнейшей и определяющей сущностью для адресной арифметики, для неё нет ничего важнее.
Если посмотреть, во что транслируется
fun
, то мы увидим следующее (ссылка на godbolt):Откуда компилятор знает, что надо на 8 "шагнуть"?
Он совсем не знает, откуда вообще взялся этот указатель: на элемент массива он изначально указывает, на поле структуры или вообще, на отдельную переменную.
Более того, могут быть различные вызовы этой, одной и той же, функции с указателями на различные, в смысле предыдущего абзаца, объекты, а обработка-то должна быть какой-то одной: указатель не несёт в себе информации о том, в составе чего находится объект.
Поэтому на что адресной арифметике точно плевать, так это – на происхождение указателя.
Но ей точно не плевать на размер типа данных, на который указывает указатель.
Именно потому, что размер
struct S
составляет 8 байт, и только поэтому, численное значение указателя увеличивается на 8.В состав чего входит объект, а также то, как он устроен внутри, имеют для адресной арифметики строго нулевое значение.
Для неё важен исключительно размер элемента и только он.
Вот пример (ссылка на godbolt):
Если раскомментировать закомментированный код, то, несмотря на то, что функции
fun0
иfun1
станут идентичными, сразу же возникнет ошибка:Во время определения функции
fun0
структураstruct S
ещё не определена, она только объявлена, а это значит, кроме прочего, что неизвестен её размер.Именно потому, что адресная арифметика не существует без размера элемента, ругань относится не только к операции
sizeof
, но и к операции инкремента указателя.Но как только структура определена, и становится известен её размер, у компилятора, при компиляции функции
fun1
, претензии пропадают не только к операцииsizeof
, но и к инкременту указателя.А что изменилось-то?
Стал известен размер
struct S
.И этого оказалось совершенно достаточно.
То есть:
Для работы адресной арифметики строго необходим размер типа, на который указывает указатель.
Кроме размера типа, адресной арифметике больше ничего не нужно, будь то происхождение объекта или его внутренняя структура, – это никак не влияет на адресную арифметику.
Я Fortran'а не знаю, особенно современного, поэтому не догадался, что значит эта 1.
В программе на C я этот тип за'
typedef
'ил, и, получается, совсем не зря.C-щный код с
char
'ом (ссылка на godbolt):gcc:
Похоже, код идентичен с Fortran'овским, с точностью до используемых регистров.
clang:
Первичный цикл у clang'а очень большой
Шаг равен 1024, не постеснялись они так развернуть.
Вторичный цикл сильно меньше:
Шаг, насколько я понимаю, 128.
Есть и третичный, совсем маленький:
Шаг – 16.
Не знаю, насколько это эффективно для относительно небольших массивов, но clang здесь отличлся.
icc (если я правильно нашёл место):
Похоже, icc не справился.
Итак, если взять GNU, то результат одинаковый, что для Fortran'а, что для C.
Опять получается так, что у Fortran'а нет преимуществ перед C в смысле быстродействия.
Каждые 5 лет в IT сменяется эпоха.
За 30 лет сменилось 6 эпох.
Также тот справочник мог быть низкого качества.
Ещё как буду (ссылка на godbolt):
Результат:
Заметьте, все компиляторы единодушны.
Размеры указателя и массива
x
отличаются.Это потому, что массив есть "смежно выделенное непустое множество объектов с определенным типом объекта-члена, называемым типом элемента", а никакой не указатель.
10 смежных
char
' ов – вот вам иsizeof
, равный 10.Если применить определение из стандарта, то никакой это не указатель на указатель.
Лучше взять
char x[10][20];
, чтобы проще отличать было.x – это 10 смежно выделенных элементов, каждый из которых есть
char y[20];
.А
y
– (условный, конечно,y
) – это 20 смежно выделенных элементов, каждый из которых естьchar
.То, что существует неявное преобразование от массива к указателю на его первый элемент, не делает массив указателем.
Да, эквивалентно, и поэтому можно написать так:
И ни один компилятор даже не пикнет (я не шучу).
Но это тоже никоим образом не превращает массив в указатель на его первый элемент.
Вот странно.
Вроде и место в стандарте показал, где массив определяется, а до сих пор слышу, про какое-то нарушение.
Смотрите, а внаглую 10-кратно разыменовал 10-мерный массив (ссылка godbolt):
и ничего не упало ни в каком из компиляторов.
Никаких, понимаете ли,
SIGSEGV
:Оптимизацию специально отключил, чтобы в ассемблерном коде видно было, что в массив кладётся 33 ('!') при инициализации и потом читается при разыменовании, а также распечатал размер массива, чтобы было видно, что он и правда, 10-мерный.
Да, интересный способ вычислить факториал 10...
Массив – не указатель.
Видите, я следую стандарту, и у меня ничего не падает.
И никаких лишних обращений к памяти у меня нет.
Неужели вы продолжите утверждать, что массив массивов - это массив указателей?
Вот бы ещё и в статье бы было так сказано...
Вот в это уже вполне верится.
К тому же, C, и особенно C++, – не для использования новичками.
А почему я не могу обнаружить этого в стандарте?
Вот, где определяется, что такое массив (ссылка на draft C11):
Всё.
Где здесь сказано, что массивы бывают многомерные, и, тем более, что тогда они должны быть массивами ссылок?
В C нет многомерных массивов.
Зато там есть массивы массивов.
Это можно продолжать рекурсивно.
У меня в изначальном примере не 5-мерный массив, а "5-мерный".
Это значит, что, на самом деле, там массив массивов массивов массивов массивов
int
'ов.Получается, это немного лучше, чем C-шный
_Generic
, но явно слабее чем C++. Но если этого хватает, то – и ладно.В статье утверждалось, что Fortran значительно быстрее C/C++, но увидеть этого мне пока не удалось.
По эффективности я не вижу преимуществ Fortran'а перед C.
Да, давайте вернёмся (ссылка на godbolt):
Программа слегка подрихтована, чтобы M и N всегда были одинаковыми и равными таковым в последующем коде на C, но чтобы выглядели как настоящие достаточно случайные.
gfortran:
Уже известная тройка инструкций и затем
add rcx, 4
.В сообщении компилятора написано:
И в этом отрывке, если проследить, то видно, что обращения, действительно, 4-х байтовые (
DWORD PTR
).Это – не векторизация, компилятора только вид сделал, что векторизовал.
У flang-trunk код идентичен.
У ifort – простыня:
Простыню под спойлер убрал
Зато здесь шаг, как видно из инструкции
addr 10, 32
, – 32, и обращения – 32-байтовые (YMMWORD PTR
).Да, это уже – векторизация, причём, довольно мощная.
Как видно, gfortran не справился, 4-байтный "вектор" – это не векторизация.
Теперь посмотрим C-шный код, максимально близкий к рассматриваемому Frotran'овскому (ссылка на godbolt):
gcc:
gcc справился, шаг равен 16, что также подтверждается 16-байтовыми обращениями (
XMMWORD PTR
).clang:
clang справился лучше всех: шаг 64 байта.
icc:
icc – хуже всех, но 8 – это, всё-таки, не 4.
Итак, что мы видим?
C ни в какой разнос не пошёл.
C-шный код векторизуется лучше, чем Fortran'овский.
C-шный код ещё и транслируется в более понятный ассемблер.
Выходит, нет у Fortran'а преимуществ перед C в смысле векторизуемости.
И какой пункт стандарта он нарушает?
Все компиляторы так делают.
Добавил MSVC (ссылка на godbolt).
clang:
Intel:
MSVC:
Смотрите, — все компиляторы так делают, у всех одно-единственное обращение к памяти.
Может, всё-таки, дело не в нарушении стандарта?
Вообще-то, сообщество настолько огромно, и у него такое количество глубоко разбирающихся в вопросе людей, что подобное "нарушение" было бы вскрыто в короткие сроки и давно исправлено.
В C нет многомерных массивов, зато есть массивы массивов.
Так "дырявые" же структуры, а не массив.
А массив состоит из структур, а не из элементов структур.
Вот пример (ссылка на godbolt):
Результат:
Видно, что сумма размеров полей структуры на 3 байта меньше размера структуры.
В структуре – дыра, причём, с краю, а не в середине, но она принадлежит структуре, входит в неё, является её неотъемлемой частью.
Именно поэтому размер структуры равен 8.
Массив структур в качестве элементов имеет объекты типа
struct S
размером 8 байт.Количество элементов – 3.
Размер массива – 24.
Между элементами массива, каждый из которых имеет размер 8 байт, нет никаких дыр.
Они есть внутри каждого элемента, но это – внутреннее свойство типа элемента, а не свойство самого массива.
Адресная арифметика "щёлкает" на размер элемента и абстрагируется от его внутренних свойств.
Поэтому ей все равно, есть дыры в каждом из элементов, нет, сколько их и какого они размера.
Единственное, что её "заботит" – размер элемента.
Точно моя?
Здесь имеется ввиду, что тип у всех объектов должен быть одинаковый.
И объекты не просто размещённые, а смежно или непрерывно размещённые.
Переводчик №1 (ссылка): "Тип массива описывает смежно выделенный непустой набор объектов с определенным типом объекта-члена, называемым типом элемента".
Переводчик №2 (ссылка): "Тип массива описывает непрерывно выделенный непустой набор объектов с определенным типом объекта-члена, называемым типом элемента".
"Смежно/непрерывно выделенный непустой набор объектов".
В отличие от структур, о которых можно прочесть в следующем предложении стандарта:
Переводчик №1 (ссылка): "Тип структуры описывает последовательно выделенный непустой набор объектов-членов (и, при определенных обстоятельствах, неполный массив), каждый из которых имеет опционально заданное имя и, возможно, отдельный тип".
Переводчик №2 (ссылка): "Тип структуры описывает последовательно размещенный непустой набор объектов-членов (и, в некоторых случаях, неполный массив), каждый из которых имеет необязательно указанное имя и, возможно, отдельный тип".
Для массивов размещение смежное/непрерывное, а для структур – лишь последовательное (упорядоченное по порядку объявления полей) без требования непрерывности размещения.
И тип элементов – для массивов один и тот же, для структур – нет.
Вы уже приводили пример PDP-11, и выяснилось, что никакой проблемы нет, ибо наличествует и адресация байтами, и в этом случае доступны и нечётные адреса.
Чья-либо уверенность в данном случае ничего не проясняет.
Тем более, если человек не работал с языком C.
Нет, не бывают.
В принципе, там и многомерных не бывает, бывают массивы массивов.
Процессор там такой (ссылка на godbolt):
Intel(R) Xeon(R) Platinum 8375C CPU @ 2.90GHz
или такой:
AMD EPYC 7R32
Но, в любом случае, чудес не бывает, одной инструкцией
paddb
обойтись не получится."5-мерный" массив (передаётся указатель на массив):
Результирующий ассемблерный код (ссылка на godbolt):
Сколько обращений к памяти?
Я вижу одно, в предпоследней инструкции.
Наверное, у того аспиранта массивы неправильные.
Грабли как правило заключаются в отсутствии владении языком, неэффективным его использованием, а не недостатком языка.
Нет, он транслирует сложение массивов ровно в те же векторные инструкции (в количестве 3-х), в которые транслируется и та программа на C, которую я приводил:
Ссылка на godbolt с вашим кодом, я оттуда достал этот фрагмент из ассемблерного листинга для компилятора gfortran (добавил опцию
-march=native
, чтобы более эффективные векторные инструкции использовались).Однако, и flang, и ifort (в последнем надо как следует поискать нужное место), выдают для сложения матриц ровно такой же код с точностью до конкретных используемых регистров.
Видите, выясняется, что Fortran не имеет преимуществ в производительности перед C.
И что?
Язык C – общего назначения, в него не тащат, что не попадя.
Если перейти на C++, Fortran проиграет, ибо C++ позволяет создать себе инструмент, причём такой, какой надо, и пользоваться им.
В Fortran'е же, насколько я понимаю, есть только то, что есть.
Ссылка на draft С11.
6.2.5 Types
...
20 Any number of derived types can be constructed from the object and function types, as follows:
— An array type describes a contiguously allocated nonempty set of objects with a particular member object type, called the element type.
...
На всякий случай, ссылка на перевод слова contiguous.
Тогда все массивы, а также области памяти, интерпретируемые как массивы, будут "дырявыми".
Но в C они не могут быть "дырявыми".
Невозможность обратиться по нечетному адресу характерна лишь для регистров R6 (PC) и R7 (SP).
Для остальных регистров проблемы обратиться к байту, в том числе, по нечётному адресу, нет.
Очень просто представляю: байтовые инструкции.
Pascal – очень условный "близнец" C.
А как ещё иначе?
Авторитетов же не существует.
К тому же, чтобы навыки не "загнивали", их необходимо тренировать, а на это так и так уходит время.
Это, видимо, потому, что я возражал по конкретному пункту о необходимости явно прописывать адресную арифметику, а потом по конкретному другому пункту касательно "устройства" массива массивов.
Ну, то есть, не требуется прописывать адресную арифметику, и компилятор может соптимизировать, поскольку "понимает", с чем работает.
Во-первых, это никакой не трюк, а штатная возможность, абсолютно переносимая и обязанная одинаковым образом быть поддержанной всеми компиляторами, претендующими на соблюдение стандарта.
А, во-вторых, основной фоновый контекст у нас здесь – преимущество (или отсутствие такового) Fortran'а перед C в рассматриваемой части.
Вы хотите сказать, что компилятор Fortran'а аналогичный код в тех же условиях сумеет векторизовать?
Макрос
ELEMS
работает для любых случаев.Элементы массива (и сам массив, а также массив массивов и далее рекурсивно) выровнены согласно требованию к выравниванию для типа элемента массива.
Между элементами массива нет "пропусков", они расположены "вплотную" друг к другу, и sizeof массива строго равен количеству его элементов, умноженному на sizeof его элемента.
Расположение элементов массива "влотную", без "пропусков" гарантируется стандартом, и это необходимое условие для того, чтобы адресная арифметика была в принципе работоспособной.
Макрос
ELEMS
прямо опирается на гарантии стандарта и именно поэтому работает для любых случаев.В структурах между полями могут быть пропуски, но там и типы у полей могут быть разными, и, соответственно, у них могут быть разные размеры и требования по выравниванию, что и может приводить к подобным эффектам.
Нет необходимости переходить к
M
иN
, отказываясь от данного макроса по надуманной причине, поэтому у "векторизатора" затруднений, связанных с этим, не появится.Язык C предоставляет средства для реализации повышенных требований к выравниванию, поэтому здесь также нет никаких проблем.
Короткий пример (ссылка на godbolt):
Результат (взятый у clang'а, потому что у gcc он не интересный):
По адресу видно, что
a0
реально выровнен на границу 16, аa1
– уже на границу 64.Явный код для выравнивания на 64 для массива
a1
можно также увидеть в ассемблере у каждого из компиляторов.Но доказательств для этого потребовалось больше, чем, я думал, будет достаточно.
Я не могу судить о реальной жизни в широком смысле, ибо недостаточно информации, поэтому не могу согласиться или не согласиться.
Да, junior'ы не смогут такое написать, и даже, наверное, часть middle'ов не сможет, особенно вариант с VLA, но достаточно им по-настоящему объяснить, как устроены типы в языке, и на чём базируется адресная арифметика, а не как это часто делают современные преподаватели, и, после некоторого времени, потраченного на практику, такое даже junior'ы смогут, здесь нет чего-то заумного.
Использование указателя на массив никак не ограничивает инкапсуляцию, а у программиста на C++, кроме указателей, ещё наличествуют в активе и ссылки, он может использовать ссылку на массив.
Поскольку речь идёт об инкапсуляции, то реализация может быть любой.
Если кто-то ничего не знает, кроме как о
std::vector
, (как ещё понять "более совместимое со стандартной библиотекой", но при этом не дающее возможности применить указатель на массив?) это не повод огульно обобщать на большинство (как ещё понять "программист на С и особенно С++ будет выбирать"?).К тому же, сейчас активно используется такая процедура как review, что нивелирует огрехи, которые могут допустить не слишком искушённые программисты.
Повторюсь, это никакой не трюк, это совершенно штатная и абсолютно переносимая возможность языка.
Там будет та структура данных, которую выберет программист.
Если программист слабо владеет языком, он легко может выбрать неподходящее решение.
Для этого обычно в компаниях бывают программисты, кототрые хорошо владеют используемым языком, и которые на review укажут, как следует изменить решение, чтобы оно стало эффективным.
И уже после этого там будет та структура данных, которую выберет грамотный программист, и которая эффективна для используемого языка.
То есть, получается, что в этом месте C имеет преимущество перед Fortran'ом, поскольку даёт больше возможностей и более гибок.
Выходит, это единственное преимущество Fortran'а в данной части.
Почему вы решили, что именно "пришлось"?
Смотрите, как можно совсем без `void *`:
Результат:
Верно, это же C, и вы сами выбрали случай, когда размерности становятся известны только в run-time'е.
Их в любом случае необходимо передавать, только в других языках это происходит "под капотом", и отменить это нельзя, а здесь, хоть и требуется выполнять явную передачу, но, зато этим можно управлять (передавать или не передавать, если не требуется).
Где здесь, особенно в последнем варианте, адресная арифметика? (1)
Что значит, "полувручную"? (2)
Функции
malloc
/calloc
/realloc
возвращают адрес с выравниванием, достаточным для хранения любого стандартного объекта.Если же передаётся адрес настоящего массива, то он, по определению, выровнен.
Зачем вы пытаетесь найти изъяны там, где их нет?
Возьмём следующий код (ссылка на godbolt):
и посмотрим, векторизуется ли он в функциях
add
иfill
.Функция
fill
заполняет массив одним и тем же значением.Функция
add
складывает поэлементно два массива и поэлементно же кладёт результат в третий.В данном коде во все элементы массива
b
кладётся 2, во все элементы массиваc
кладётся 3, затем они суммируются, а результат кладётся в массивa
.Первый элемент массива
a
распечатывается, чтобы icc не "думал", что, раз массивa
после заполнения никак не используется, то можно ничего и не вычислять, и не выбрасывал вызов функцииadd
.В функции
add
видим в ассемблерном коде gcc следующее:Видите векторизацию?
Компилятор смог.
В функции
fill
видим в ассемблерном коде gcc следующее:А здесь видите векторизацию?
Компилятор опять смог.
Для clang'а и icc от Intel'а всё аналогично.
Если прогнать perf'ом, то получается следующее (у меня локально установлена другая версия gcc, поэтому используемые регистры в ассемблерном коде слегка другие):
Видно, что эти инструкции на самом деле работают, а не просто "валяются в коде рядом".
Это означает, что компилятор смог.
Более того, смогли все опробованные мной компиляторы.
Вы можете ответить на вопросы, помеченные мной как (1) и (2)?
Итак:
В C массив массивов понимается как массив массивов, причём независимо от того, какой это массив, обычный или VLA.
В этом стиле доступна работа и с динамически выделенной памятью, причём даже в этом случае размерности каждого массива не обязаны быть константами времени компиляции.
Никакой дополнительной адресной арифметики при этом не требуется.
Никаких прочих накладных расходов, связанных с использованием указателей на
void
, нет.Передача размерностей массива в рассматриваемом случае не зависит от языка и будет присутствовать в том или ином виде в любом языке.
Не существует никаких проблем с выравниванием, специфичных для применяемой техники.
При этом все широко используемые современные компиляторы прекрасно векторизуют код.
В этом смысле Fortran не имеет преимуществ перед C.
Если ещё учесть, что strict aliasing'ом можно управлять с помощью ключевого слова
restrict
, то станет очевидно, что в этом смысле Fortran вообще не имеет преимуществ перед C.Напоминаю, что первый пункт в списке выше есть прямое возражение на ваше утверждение:
Да, давайте ещё и константность везде поубираем, чтобы она вас не смущала.
И чтобы компилятор не смог ничего лишнего соптимизировать, запутаем способ получения
M
иN
, да так, чтобы в выражениях для их вычисления ещё и ни единой константы не было.Вариант с VLA:
Это я знаю, что программа будет запускаться без параметров, и
argc
будет равен1
.Но компилятор из этого не может исходить, тем более, что я же могу второй раз ту же уже скомпилированную программу и с параметром запустить.
Результат:
Вариант с динамически выделяемой памятью:
Результат:
Есть ещё идеи?
В C массив массивов понимается как массив массивов, причём независимо от того, какой это массив, обычный или VLA.
Более того, в этом стиле доступна работа и с динамически выделенной памятью, причём даже в этом случае размерности каждого массива не обязаны быть константами времени компиляции.
В этом смысле Fortran не имеет преимуществ перед C.
Сделал:
Результат:
Предвосхищая возможное дальнейшее предложение поработать в таком же стиле с динамически выделенной памятью, сделаю и это:
Результат:
В C массив массивов понимается как массив массивов.