Comments 99
> «Часто будет выгоднее обратиться 100'000 раз к оперативной памяти, чем 1 раз к диску.»
Но ведь 100.000 обращений к оперативной памяти делается операциями, которые сами по себе займут 100.000 * x тактов? Как часто можно встретить ситуацию, когда 100.000 раз обращений к оперативки выгоднее, чем одно обращение к диску, особенно учитывая существование SSD и PCI-SSD?
> Скорость обращения к удаленному серверу «1'000'000'000 тактов»
Как можно скорость отклика (ping) мерять тактами? Тем более, что сетевой сокет в гигабитной локалке, может быть быстрее, чем обращение к диску. Я могу расшарить на соседнем компе виртуальный ram-диск, и по гигабитной сети это будет совсем не 1.000.000.000 тактов.
Мы же не считаем непосредственно доступ к памяти, а учитываем количество обращений со всем прилагаемым — сами операции обращения?
Вот ниже картинка
прочитать 1.000.000 байт из памяти — 6000 нс
прочитать 1.000.000 байт с диска — 1.000.000 нс
Итого, разница в 166 раз, но никак не в миллион.
Кроме того Ваш оппонент говорил о кэше L1, который на два порядка быстрее чем память DRAM о которой говорите Вы. Правда мегабайт туда не влезет, но он влезет в L3 который все равно на порядок быстрее.
Не надо передергивать.
В статье многобукафф, но не заметил инфы про ассоциативность. Есть размер кэша, да, но есть еще другого рода ограничение. Если обрабатывать в разных частях памяти блоки, то для конкретного процессора не более K штук таких блоков одновременно эффективно кэшируются (проверял лично).
Я могу расшарить на соседнем компе виртуальный ram-диск
С трудом верится, ибо гигабитная сеть, она про трупут. А цифры из статьи про лэйтенси.
Хотя, мне даже интересно, какое время отклика было бы у такой связки, но нет физически близкой машины такой под рукой :)
Не знаком с темой — что такое IB? Гуглится что-то странное.
Это 10g сетевухи от melanox и myricom?
Это InfiniBand, обычно 10+Gb/s (оно с агрегацией) и субмикросекундными latency.
64 чтения из областей, не входящих в одну выровненную 64-байтную область (как строка кэша), займут ~64*250 тактов. 64 чтения подряд из одной такой области займут, по описанной таблице, 64*4 такта. Достаточно просить prefetch на одну такую строку в начале чтения предыдущей, чтобы последовательное чтение не задерживало процессор. В реальности ситуацию ещё больше улучшают микросхемы памяти с несколькими одновременно открытыми строками и многоканальные контроллеры памяти.
> Как часто можно встретить ситуацию, когда 100.000 раз обращений к оперативки выгоднее, чем одно обращение к диску, особенно учитывая существование SSD и PCI-SSD?
При хоть сколь-нибудь продуманном доступе к памяти — считаем, практически всегда.
> Тем более, что сетевой сокет в гигабитной локалке, может быть быстрее, чем обращение к диску.
Гигабитный — по сравнению с SATA даже 1-м — уже нет :) Вот 10GB, или Infiniband'овские скорости — да, уже стоит упоминать.
Гигабитный — по сравнению с SATA даже 1-м — уже нет :) Вот 10GB, или Infiniband'овские скорости — да, уже стоит упоминать.
Не важно гигабит или 10. Проблема не в пропускной способности, а в сетевых задержках.
Я рассматривал в контексте передачи сравнимого с от «100,000 обращениями к оперативной памяти» и до «1,000,000,000 тактов» объёма передачи, там задержки уровня одного пакета уже не столь существенны, а скорость значительно больше влияет на общую задержку. В контексте коротких передач — да, безусловно, суммарные задержки важнее, и пример «512 байт из iRAM по сети vs. HDD локально, которому надо ещё переместить головки на полный диапазон» — отличная иллюстрация.
Прогноз тоже не впечатляет, через 5 лет практически ничего не поменяется, а дальше неизвестность.
Я, просто, не видел ничего нагляднее.
А тут ведь ещё бонусом «интерактив» и чуть-чуть истории.
Upd: автор этой странички не я. Я только заскриншотил, «мопед не мой. Но мопед хороший, годный!!!»
Есть ещё вот такая аналогия, текстовая:
1 CPU cycle: 0.3 ns => to human scale 1s
L1 cache access: 0.9ns => 3s
L2 cache access: 2.8ns => 9s
L3 cache access: 12.9ns => 43s
Main memory access: 120ns => 6 min
SSD Disk: 50-150 us => 2-6 days
Rotational Disk: 1-10ms => 1-12months
Internet: San Francisco to New york: 40ms => 4 years
И вот такая вот инфографика.
Вообще — тема довольно избитая :)
В данном графике, скорость памяти зависла на отметке в 100 нс, и меняется только скорость SSD, что наталкивает на заказной характер графика со стороны производителей SSD.
Кроме того на скорость работы памяти (кроме непосредственно характеристик модуля памяти) влияет количество каналов памяти в системе, и оптимизации структур хранения данных под канальность.
Пусть будет 50. Добавьте к этому время кеш-миссов L1-L2-L3
Но конкретно эта — полностью провалена =(
Тут даже с текстом не сразу разобрать что имел в виду автор.
Двигаем ползунок по года и лишь видим, как скорость доступа к данным ускорялась со временем. Для этого лучше бы простой график сделали, и то было бы нагляднее.
Как программист, вы должны понимать иерархию памяти, потому что она сильно влияет на производительность ваших программ. Если вы понимаете как система перемещает данные вверх и вниз по иерархии, вы можете писать такие программы, которые размещают свои данные выше в иерархии, так что процессор может получить к ним доступ быстрее.Как программист, я не должен! Это компилятор должен оптимизировать мой код, и часто у современных компиляторов это не плохо получается. Но как программист имею право отказаться от решений компилятора и попробовать сделать код быстрее. В этом случае статья может помочь, но таких случаев небольшой процент от всех решаемых задач.
У компиляторов это получается на уровне переменных. Но компилятор не сможет выбрать направление обхода матрицы.
Первая функция читала элементы матрицы построчно и двигалась по памяти последовательно, будто обходила один большой одномерный массив.Это убедительно. Но ведь это тривиальный случай.
Это довольно частый случай.
статья может помочь.
Как программист, вы должны
А Вы написали:
Если вам нужна максимальная производительность
Видите разницу? Я только об этом. Если моя программа работает недостаточно быстро, то мне нужно подумать, как ее ускорить. А если быстро, то не стоит тратить время на ненужное ускорение. Иногда авторы используют неподходящую форму и слова, получая нежелательный для автора эффект.
Если моя программа работает недостаточно быстро, то мне нужно подумать, как ее ускорить.
Совершенно верно. И это входит в обязанности программиста.
«обычно 90% кода тратят 10% общего времени работы программы». Для Ваших задач это утверждение справедливо? Если справедливо, то должен ли программист думать об иерархии памяти, когда работает над некритическим участком?
Поставить индексы в правильном порядке, сгруппировать данные, поставить parallel.for — эти действия практически не увеличивают время написания программы, зато сильно уввеличивают скорость выполнения. Поэтому программист должен уметь всем этим пользоваться, чтобы в случае, когда придётся писать критический участок, действия были доведены до автоматизма.
А вот выжать максимум — задействовать AVX, переписать код на CUDA может увеличить время разработки в несколько раз, что сведёт на нет выгоду от быстрого выполнения вычислительного кода. Я, например, вычислительные алгоритмы вообще пишу на C#, потому что так быстрее. Когда возникает необходимость ускорить — переписываю на C++ с использованием AVX. CUDA — тяжёлая артиллерия, когда CPU уже мало.
// A*x -> y (x !=y !!!!)
void Slau::amulx(const double *x, double *y)
{
int i, j, t;
// y = 0
for (i = 0; i < N; i++)
y[i] = 0;
// вне диагонали
for (j = 0; j < N; j++) // строка номер j
{
for (t = ind[j]; t < ind[j + 1]; t++) // t — индекс элемента j-й строки
{
i = ai[t]; // столбец номер i
// a[t] — элемент A(j, i)
y[j] += a[t] * x[i];// нижний треугольник
y[i] += a[t] * x[j];// верхний треугольник
//printf(«A(%d, %d) = %lf\n», j, i, a[t]);
}
}
// диагональ
for (i = 0; i < N; i++)
y[i] += d[i] * x[i];
}
Скопировал как есть, написано года ~3 назад.
А это <1% кода. Только сейчас задумался, как можно это оптимизировать. В boost строчно-столбцового формата для симметричных матриц вроде бы нет (за бугром его не юзают почти), а на ассемблер лома переписывать и ассемблер — это временное решение.
1. Переписывание на ассмеблере может дать эффект только если вы программируете под компьютеры 25-летней давной. С современными компиляторами и инструкциями процессоров, скорее всего, сделаете только хуже.
2. Использование свойства симметричности матрицы снижает потребление памяти, но ухудшает производительность. Храните матрицу в общем виде, не используя этого свойства.
3. Попробуйте использовать специализированные оптимизированные библиотеки для решения поставленной задачи (например, Intel MKL), если самостоятельная реализация алгоритмов линейной алгебры не является
Переписывание на ассмеблере может дать эффект только если вы программируете под компьютеры 25-летней давной. С современными компиляторами и инструкциями процессоров, скорее всего, сделаете только хуже.И я того же мнения, но всего несколько месяцев прошло как в другой теме меня хором уверяли:
А критические участки реализуют на ассемблере по сей день, и если что — я и мои знакомые можем делать на ассемблере код более быстрый, чем на языке высокого уровня :)Какие противоположные мнения существуют.
Про MKL. Не всегда помогает. У меня был случай, когда надо было решать очень много небольших (в десяток неизвестных) СЛУ. Мой код на языке высокого уровня оказался для такого случая быстрее, чем MKL. Спрашивал разработчиков — подтвердили, что это так для моего специального случая.
Что касается MKL. Ну да, за универсальность тоже стоит платить. Например, собственная реализация Parallel.For имеет существенно меньший оверхед, чем реализация в Intel TBB, что позволяет вообще не думать о гранулярности параллелизма в задачах обработки изображений.
оффтоп
1. Переписывание на ассемблер в теории (в идеале) даст максимальную производительность, для конкретного множества процессоров, то есть временно, поэтому да, толку от этого мало.
2. Использование свойства симметричности наоборот улучшает производительность, это очевидно из приведенного кэш-френдли кода.
3. Таки да, придется использовать обычное строчное представление CSR и PARDISO (Intel MKL) / Eigen, за неимением лучшего.
На моем прошлом проекте (embedded), мы дрались буквально за каждый такт.
Отдельная история состоит в том что для FP-вычислений оптимизация может менять результат вычислений, поэтому многие оптимизации подобного рода по умолчанию заблокированы. Интел по-моему единственный кто их по умолчанию включает (и у меня в проекте он дооптимизировал подобным образом один кусок legacy-кода до segfault :) )
double mul = x * y;
double frac = mul - floor(mul);
Безобидный код, правда? И из него совершенно очевидно что frac >= 0. Так собственно всю жизнь и было, пока не пришел Интел и не сказал
r1 = load(x)
r2 = load(y)
r3 = mul(r1,r2)
r4 = floor(r3) // r4 = floor(x*y)
r5 = fmsub(r1, r2, r4) // r5 = x*y - r4
frac = store(r5)
Черт его знает зачем компилятору vfmsub213sd в код захотелось вставить, но из-за того что FMA хранит результат умножения с большей точностью чем регистр то в некоторых случаях получилось что frac < 0 и это крэшило код дальше который предполагал что отрицательного числа в frac никогда не будет.
незаметных ускорениях кода
А вы читали статью?
Объекты представляются как вершины, или узлы графа, а связи — как дуги, или рёбраСовсем дикие?
если представить матрицу визуально, то элементы (i, j) и (j, i) являются симметричными относительно диагонали.
В квадратной матрице две диагонали. Речь здесь идет о главной диагонали. В литературе по матричной алгебре не пишут, что матрицу нужно представлять визуально, а просто пишут, что матрица симметричная.
В квадратной матрице есть только одна диагональ, потому что вторая не имеет математического смысла.
Вы, если программист, можете убеждать недовольное быстродействием вашей программы начальство или клиентов в том, что обеспечить быстродействие программы — это не ваша обязанность, а обязанность компилятора. С некоторыми людьми такое может даже сработать.
Чтобы все работало быстро, на плюсах нужно знать довольно много нюансов, начиная от передачи тяжелый объектов по ссылке, move-семантики и префиксного инкремента, и заканчивая специфичными оптимизациями компилятора типа __builtin_expect. К слову, последнее на уже давно написанном и оптимизированным коде выдало 20% прирост производительности на x86 и (sic!) двукратный прирост на VLIW архитектуре.
Наряду с этими оптимизациями, надо еще писать эффективный параллельный код, то есть знать, что переключение контекста процессора — это ОЧЕНЬ плохо и дорого, что нужно использовать барьеры памяти, RW-локеры, атомарные операции, lock-free структуры и кучу другой магии. А еще нужно знать структуры данных, алгоритмы, оценивать сложность разработанных алгоритмов и т.д. Совокупный кругозор по всем этим тематикам делает из конкретного программиста крутого спеца, за которого готовы платить очень серьезные деньги такие компании, как Яндекс, Интел, Гугл, Майкрософт и далее по списку.
Естественно, это нужно не всем. Естественно, не нужно оптимизировать цикл, если он состоит плюс-минус из 5 элементов. Но есть ряд задач, где все эти хаки и знания приносят ощутимую выгоду.
Раз уж пошла такая пьянка, то может быть кто-то знает ответ на мой вопрос:
http://stackoverflow.com/q/39947921/253146
Коротко: почему выделение памяти операционной системой для процесса занимает в 2 раза больше времени, чем очищение страниц в оперативной памяти + очищение страниц в L3 кеше процессора.
Вы очищаете память два раза. Первый раз вам ее чистит система, перед тем как вернуть чистую страницу — второй раз вы сами чистите ее memset. Отсюда и просадка производительности.
На больших блоках malloc/free начинают запрашивать память напрямую у системы — и так же возвращать обратно.
Вы очищаете память два раза.
Да, об этом там написано
На больших блоках malloc/free начинают запрашивать память напрямую у системы
Да, об этом там написано
Отсюда и просадка производительности.
Нет, не отсюда. Производительность при выделении памяти у системы все равно в 2 раза хуже, чем очищение памяти не влезающей в кеш процессора + очищение памяти, влезающей в кеш.
Вы понимаете что память возвращаемая в ОС чистится. Это 2x потеря производительности
Вы понимаете что память полученная от ОСи не замаплена изначально в физическую. Это означает получение PAGE FAULT на каждые 4 Кб памяти и соответствующее прерывание — это отжирает еще приличный кусок производительности.
Вы, по идее, понимаете что выделение блока адресного пространства и каждой из страниц физической памяти требует от операционки поиска свободного пространства / страниц, исправления таблиц трансляции и обновления их в TLB. Это все операции подразумевающие довольно обширные прогулки по памяти и потому — сравнительно медленные и подъедающие еще ощутимый кусок от производительности.
Я правильно понимаю что судя по последнему абзацу SO вопрос, в общем-то, сводится к тому «а почему это все не сделано быстрее»? Потому что ответ на него очень простой: универсального решения в любом случае не существует и создатели операционки просто перекладывают его на разработчика программ. Если Вам внезапно почему-то нужно постоянно аллоцировать и освобождать крупные блоки памяти, то Вы должны использовать подходящий под эту задачу менеджер памяти, а не надеяться что операционка угадает какой именно из (разных возможных) сценариев использования памяти оптимальным образом подойдет к Вашему приложению. Дефолтная реализация в операционке консервативна, экономит физическую память и написана максимально безопасным способом.
PAGE FAULT на каждые 4 Кб памяти и соответствующее прерывание
поиска свободного пространства / страниц
Спасибо, это уже похоже на возможное объяснение.
«а почему это все не сделано быстрее»
Это такой теоретический подвопрос. Цель у меня сугубо практическая. Мне действительно иногда нужно выделать по 40-100 мегабайт памяти за раз и я хочу знать, можно ли с этим что-то сделать, кроме как тупо не отдавать память обратно. Мне бы сгодилась даже какая-нибудь настройка ядра Линукса, которая магическим образом делала бы все небезопасным но быстрым.
Я полагаю, это оверхед от переключения контекста между приложением и ядром при pagefault-ах. Можно попробовать использовать mmap
c разными флагами (MAP_POPULATE например).
Потому что от момента когда статья была прочитана до момента, когда она пригодилась, может пройти довольно много времени. Мне вот не раз пригождались статьи в интернете — но каждый раз я к тому моменту забывал где я это все прочитал.
Варианта написания тут два:
1. Подход наивный — для каждого пикселя 9 раз вычислять производные, не переиспользуя значения.
2. Подход с наименьшим количеством операций — это сначала посчитать модуль производной в каждом пикселей, затем усреднить по столбцам, затем по строкам. Т.е. 3 прохода по изображению с кэш-френдли доступом.
В однопоточном режиме без AVX производительность первого подхода была немного ниже, чем второго. Но когда я переписал код с использованием AVX и параллельности, сразу упёрся в скорость памяти. Второй способ оказался в ~2.5 раза медленнее первого.
Выжать максимум же можно, преобразовав второй метод в однопроходный, используя конвейер. Ну и немного позаморачиваться при параллелизации. Я, правда, не стал заморачиваться, т.к. это привело бы к ускорению максимум на 10%, зато сильно усложнило бы код.
Кеш для записи позволяет оптимизировать ее двумя основными способами.
1) передача операции записи на другое устройство/другой поток, которое выполнит эту запись по возможности, чтобы основной поток продолжил выполнение основной задачи сразу.
2) объединение нескольких мелких операций записи в одну большую, что уменьшает работу более медленных устройств.
Оптимизация кода: память