...

суббота, 12 января 2019 г.

Конференции и хакатоны для школьников и студентов: 11 тематических мероприятий

Это — подборка конференций и хакатонов для бакалавров, магистров и молодых ученых. Все они пройдут в Санкт-Петербурге (или онлайн) в ближайшую пару месяцев.


Manu Cogolludo Vallejo / Flickr / CC BY-SA



1. Студенческий трек Олимпиады НТИ

Когда: 3 декабря 2018 — 15 января 2019
Где: онлайн (финал на площадках вузов)

Соревнования среди бакалавров и ИТ-специалистов по шести направлениям: аэрокосмические системы, VR и AR, умная робототехника, беспроводная связь, умный город и прикладные системы ИИ. Последний профиль курируем мы — Университет ИТМО.

Первый этап олимпиады пройдет на онлайн-платформе Stepik. Финал состоится на площадках вузов. Среди заданий будут практические кейсы, оценка теоретических знаний и навыков командной работы. Победители олимпиады получат возможность поступить в магистратуру российских университетов без конкурса. Дедлайн для регистрации — 15 января.



2. «IT-Планета 2018/19»: XII Международная ИТ-олимпиада

Когда: 13 декабря — 20 февраля
Где: онлайн

Олимпиада для русскоговорящих студентов и молодых специалистов не старше 25 лет из любой страны мира. Состязаться предлагают в семи номинациях: программирование, облако и БД, телекоммуникации, мобильные платформы, открытое ПО и робототехника, плюс digital-творчество и неограниченные возможности. Две последние категории связаны с дизайном, 3D-моделированием и проектами, направленными на решение проблем человечества. Те, кто покажет наилучшие результаты, в июне поедут на финал в Москву.



3. Конкурс цифрового искусства NTU

Когда: 17 декабря — 15 февраля
Где: онлайн

Наньянский технологический университет (NTU) — главный технологический университет Сингапура — приглашает всех желающих (старше 18 лет) поучаствовать в конкурсе цифрового искусства. Главный приз — восемнадцать тысяч долларов.

Участникам предлагается создать арт-работу на тему «Индустрия 4.0». Есть следующие номинации: 2D или 3D, анимация, фильмы и видео, живопись и многие другие (всего доступны четырнадцать категорий). Работы можно выполнять в любом формате и жанре, главное, половину проекта реализовать с использованием цифровых медиа. Зарегистрироваться на участие можно, заполнив Google-форму до 15 февраля.



4. Конкурс научных видеороликов JuniorUni

Когда: 26 декабря — 14 февраля
Где: онлайн

JuniorUni — это бесплатный образовательный проект для подростков, открытый на базе немецкого Гёте-Института. Его задача — обучать детей робототехнике и параллельно прививать им любовь к космонавтике. На конкурс видеороликов приглашают школьников с седьмого по девятый классы.

Участникам нужно снять пятиминутный ролик, посвященный робототехнике, космонавтике или естественным наукам. Однако видео должно быть на немецком языке. Готовые работы нужно загрузить на YouTube, а ссылку отправить организаторам через онлайн-форму до 14 февраля. Среди призов для победителей: курсы немецкого в Германии, кино-дроны и iPad.




На фото: DIY-принтер из фотоэкскурсии по нашей DIY-лаборатории «Фаблаб»

5. Всероссийский кубок по менеджменту «Управляй!»

Когда: 26 декабря — 10 февраля
Где: онлайн (полуфиналы и финал — очные)

Это состязание по менеджменту, которое проходит в формате бизнес-игры. К участию приглашаются студенты российских вузов экономических специальностей в возрасте от 18 до 25 лет. Призы: стажировка в Гонконге, гранты на обучение в вузах РФ, а также техника и разный мерч. Чтобы принять участие, нужно зарегистрировать команду из 3–5 человек до 10 февраля и пройти онлайн-отбор на специальном бизнес-симуляторе.



6. UX-Марафон #14: проектирование сложных систем

Когда: 24 января
Во сколько: c 11:00 до 18:00
Где: онлайн

Специалисты по UX из отечественных и международных компаний проведут серию лекций о тестировании сложных программных интерфейсов. Среди спикеров заявлены представители «Росбанка», General Electric, «СКБ Контур» и UsabilityLab. Они поделятся личным опытом разработки и расскажут, как строить сложные интерфейсы, обучать пользователей работать с приложениями и анализировать данные бизнеса. Принять участие в онлайн-конференции могут все желающие.



7. Современные технологии: вводный курс

Когда: 28 января — 31 мая
Во сколько: c 10:00 до 18:00
Где: онлайн

Онлайн-курс из 15 уроков, которые проводит «Центр роботизации и искусственного интеллекта». Организация занимается созданием RPA-алгоритмов и нейросетей. Тема уроков: цифровизация экономики и изменение бизнес-моделей. Специалисты компании расскажут о задачах, которые решают с помощью больших данных, машинного обучения и роботизации. Разбор будут вести на реальных кейсах российских компаний.

Уроки можно проходить в удобное время. Специалисты «Центра» будут проверять домашние задания и давать обратную связь. По итогам курса все участники получат сертификат и багаж практических и теоретических знаний о трендовых разработках.



8. Кигалийская поправка к Монреальскому протоколу: ретроспектива эволюции или будущего зов?

Когда: 30 января — 31 мая
Во сколько: регистрация в 9:00, открытие в 10:00
Где: ул. Ломоносова, д. 9, Университет ИТМО, ауд. 2219

Конференция про энергоэффективные решения, которая пройдет в Университете ИТМО. Будем обсуждать системы охлаждения для дата-центров, «зеленые» технологии, проблемы изменения климата, а также углубимся в термодинамику и процессы тепломассообмена.

К участию приглашаются все желающие. Если хотите выступить с докладом по теме, то пишите на электронную почту Юрию Лаптеву. Это сотрудник факультета низкотемпературной энергетики нашего Университета (заявки принимаются до 20 января). Все доклады мы будем рекомендовать для публикации в отраслевых изданиях: «Вестник Международной академии холода», «Холодильная техника», «Империя холода» и др.



9. ITMO Open Science — День науки в Университете ИТМО

Когда: 8 февраля
Во сколько: 10:00 — 19:00
Где: 29 лин. Васильевского о-ва, д. 2, музей Эрарта

Это конференция для ученых и увлекающихся наукой сотрудников Университета ИТМО. Встреча пройдет в стенах музея современного искусства Эрарта. Преподаватели наших мегафакультетов презентуют лучшие разработки Университета ИТМО в виде электронных постеров, поделятся ключевыми достижениями в формате стендап-битвы и проведут открытое интервью на театральной сцене. После для всех участников запланирована экскурсия по музею Эрарта.



10. Международный форум Internet of Things

Когда: 21 февраля
Во сколько: 10:00 — 18:00
Где: ул. Лодейнопольская, д. 5, «ПетроКонгресс»

Этот форум будет полезен тем, кто работает в IoT-отрасли и интересуется решениями по теме. Событие пройдет в формате выставки-конференции: гости узнают свежие новости из мира интернета вещей, познакомятся с последними разработками и пообщаются с единомышленниками. Стартаперы получат возможность представить свой продукт потенциальным инвесторам и найти партнеров по бизнесу. Приглашаются все желающие.



11. Хакатон по биоинформатике BioHack

Когда: 1 — 3 марта
Во сколько: круглосуточно
Где: ул. Заставская, д. 22, лит. А, бизнес-центр «МегаПарк», офис EPAM

Хакатон для программистов, медиков, математиков и биологов. Участники должны быть студентами старших или средних курсов бакалавриата, магистрантами, аспирантами или молодыми специалистами. Темы проектов могут быть связаны с биоинформатикой или близкими к ней областями науки. Заявки принимаются до 11 февраля.

Соревнование продлится 48 часов. Организаторы предоставят питание, оборудование и неограниченный доступ к площадке. Команда победителей получит 100 тысяч рублей. За второе и третье места дают по 70 и 50 тысяч.

Мероприятие проводит международная компания EPAM, занимающаяся разработками программного обеспечения. В 2017 году она попала в рейтинг Forbes как одна из 25 самых быстрорастущих технологических компаний.



Наши фотоэкскурсии:

Let's block ads! (Why?)

Заметки фитохимика. Радио-банан

Каждое чудо должно найти свое объяснение, иначе оно просто невыносимо…
К.Чапек

Я практически не касаюсь в своих статьях вещей, которые повсеместно описаны и легко доступны, к примеру макро- и микроэлементного состава фруктов/овощей. Но вот для банана решил сделать исключение. В банане много калия! Подними любого среди ночи и спроси, что полезное есть в банане — получишь ответ "калий для сердца" (утрирую, но не далеко от истины). А калий, он элемент непростой. В общем, чтобы узнать так ли велика радиоактивность от банана и так ли она страшна — идем под кат.
p.s. заметка "по просьбам трудящихся".


Калий относится к т.н. биогенным элементам, т.е. он постоянно присутствует в живом организме и играет важную биологическую роль. В теле человека содержится около 0,35% калия. 98% из этого количества приходится на клетки, а остальные 2% — это внеклеточная жидкость (в том числе и кровь). Градиент концентраций поддерживается т.н. "Na+/K+ насосом". Факт наличия электрохимического градиента калия между внутриклеточным и внеклеточным пространством важен для работы нервной функции (реполяризация клеточной мембраны, например). При гипокалиемии (недостатке калия) вследствие замедления реполяризации желудочков увеличивается риск нарушения сердечного ритма, который зачастую может привести к остановке сердца. В общем, ясно что очень организму нужен. Поступает он, в большинстве случаев (как и другие микроэлементы) с продуктами питания.

Важно! При необходимости уточнить какие-то данные по определенным микроэлементам, я пользуюсь базой департамента США по сельскому хозяйству (United States Department of Agriculture Agricultural Research Service, оно же USDA) и вам настоятельно советую.

Так вот, по данном этой базы, в бананах примерно 358 мг калия на 100 г продукта, сравнимой "мощностью" из доступных тропических "гостей" обладает только киви со своими 522 мг калия. Все остальное достаточно редкие штуки (тамаринд — 628 мг, авокадо — 485 мг (не редкое, в суши часто встречается), дуриан — 436 мг, гуава — 417 мг, маракуйя — 348 мг). При этом сравните с родненькими "возле каждого выхода из метро" продуктами: укроп — 738 мг, шпинат — 558 мг, петрушка — 554 мг, кинза — 521 мг, даже щавель тот лесной и то 390 мг на 100 грамм продукта содержит. Есть и в овощах кой-чего: капуста брюссельская — 389 мг, тыква — 340 мг, смородина черная — 322 мг. Так что перед очередным "найти %nutrient% за 60 секунд на полке с субтропическими фруктами", гляньте по USDA базе, может все уже есть в морковке или кабачках...

В любом овоще/фрукте/зелени помимо калия, есть и его изотопы. Стабильными являются 39K (93,08% от общей массы), 40K (0,01% от общей массы, период полураспада 1,248*109 лет), 41K (6,91% от общей массы). Все остальные живут от часов до наносекунд и распадаются:


Необычен наш микроэлемент (относительно других) тем, что имеет изотоп 40K, который является редким примером изотопа, который подвергается обоим типам бета-распада. Приблизительно в 89,28% случаев он распадается на кальций-40 (40Ca) с испусканием бета-частицы (β-, электрон ) с максимальной энергией 1,31 МэВ и антинейтрино. Около 10,72% времени он распадается на аргон-40 (40Ar) путем захвата электронов с испусканием гамма-излучения с энергией 1,460 МэВ и нейтрино. Радиоактивный распад этого конкретного изотопа объясняет большое содержание аргона (почти 1%) в земной атмосфере, а также высокое его содержание по сравнению с 36Ar. Очень редко (в 0,001% случаев) он распадается до40Ar, испуская позитрон (β+) и нейтрино. Про последнюю реакцию писалось в Хабра-статье . Дескать банан-источник антиматерии.

Благодаря озвученным фактам, именно 40K является крупнейшим источником естественной радиоактивности животных, включая человека. В грамме природного калия происходит в среднем 32 распада калия-40 в секунду (32 беккереля, или 865 пикокюри или примерно одна триллионная часть кюри). Человеческое тело весом 70 кг содержит около 175 г калия, следовательно, каждую секунду происходит около 5400 распадов (≈ 5400 беккерель), притом непрерывно на протяжении всей человеческой жизни.


Беккерель (русское обозначение: Бк; международное: Bq) — единица измерения активности радиоактивного источника в Международной системе единиц (СИ). Один беккерель определяется как активность источника, в котором за 1 секунду происходит в среднем 1 радиоактивный распад. Единица названа в честь французского учёного Антуана Анри Беккереля, одного из первооткрывателей радиоактивности.

В принципе, ничего тут удивительного нет. В природе существуют и более радиоактивные продукты питания, притом радиоактивные не только из-за 40K, но и, к примеру, радия (изотопы 226Ra, 228Ra). В качестве примера отлично подойдет бразильский орех, радиоактивность которого может достигать 12000 пикокюри на килограмм и выше (450 Бк/кг и выше).

На заметку: хуже всех в этом плане приходится курильщикам, так как табак содержит не только уже упомянутый радий 226Ra, но торий 234Th, полоний 210Po и еще кучу всего.

Но почему-то товарищ Гэри Мэнсфилд из Ливерморской национальной лаборатории им. Лоуренса, делая рассылку по ядерной безопасности RadSafe в 1995 году, написал именно про "банановую эквивалентную дозу" и началась новая эпоха. Эпоха радиактивного банана (банановый эквивалент — штука гораздо более ядреная, чем банановый аргумент, описанный в статье).

Эквивалентная доза банана (BED) — это абсолютно неофициальная единица, которая характеризует воздействия ионизирующего излучения. Ее основное назначение — выступать в качестве доступного даже рядовому пользователя эталона, с которым можно легко сравнить дозы радиоактивности. Фактически это инструмент для описания бесконечно малых доз радиации (и бесконечно малых рисков для населения от них). Выдержка из Википедии (RU):


… Поскольку смерть или тяжелое заболевание, вызванное малой дозой облучения (ниже 0,5 Гр) крайне редки, выяснилось, что уверенно связать их с воздействием радиации на организм невозможно — потребуются наблюдения в течение длительного времени (более 12-ти лет) над огромной выборкой людей, подвергшихся облучению в такой дозе. Более того, было обнаружено положительное влияние малых доз радиации на живые организмы — гормезис. С малыми дозами радиации также связан феномен массового сознания, когда неопределенность в вопросе безопасности (или уверенность в том, что существующая опасность незначительна) трактуются как заведомое наличие опасности и формируется массовый страх перед малыми дозами радиации.

Пару слов про радиационный гормезис:


Термин радиационный гормезис был предложен в 1980 году Т. Д. Лакки и означает благоприятное воздействие малых доз облучения. Механизм радиационного гормезиса на уровне клетки теплокровных животных состоит в инициировании синтеза белка, активации гена, репарации ДНК в ответ на стресс — воздействие малой дозы облучения (близкой к величине естественного радиоактивного фона Земли). Эта реакция в конечном итоге вызывает активацию мембранных рецепторов, пролиферацию спленоцитов и стимуляции иммунной системы. (1994 г. — доклад Международного комитета ООН по действию атомной радиации).

Будучи воспитанником кафедры химии высоких энергий, я к понятию гормезиса (~радиационного гормезиса) отношусь с уважением (уважение, в свое время подкрепилось экспериментальной куросовой работой в одной из больниц). Но таки считаю, что маленькое но постоянно, вреднее чем большое, но единоразово. Капля камень точит.

Чтобы получше ориентироваться, что малая доза, а что НЕ малая — можно использовать, помимо бананового эквивалента и наглядное пособие — сводную таблицу доз облучения (увеличить), созданную инженером и популяризатором науки Рэндаллом Патриком Монро (прим. мое — банановый эквивалент обведен красной рамкой).


Ну а если таблица по каким-то параметрам не устравивает, возвращаемся к нашему банановому эквиваленту. 1 BED приблизительно равен дозе радиоактивности, которую человек получает при употреблении в пищу одного среднего размера банана, весом около 150 г (5,3 унции) с активностью изотопов примерно 15 Бк. Рассчитывается это все умножая ожидаемую эквивалентную дозу которую может хватануть взрослый человек за 50 лет от чистого изотопа 40K на активность изотопа и на массу калия в банане. Получаем:

1 BED ≈ 5,02 нЗв/Бк х 32 Бк/г х 0,537 г ≈ 86 нЗв = 0,086 мкЗв (µSv) = 8,6 микрорентген (μrem)

В основном принято округлять это значение до 0,1 мкЗв (10 микрорентген) для упрощения расчетов и простоты восприятия. В общем, если съедать по одному среднему банану в год, суммарная эквивалентная доза будет ≈ 37 мкЗв ≈ 3,7 мбэр.

Кстати, ожидаемая эквивалентная доза (5,02 нЗв/Бк) взята из американских источников (EPA). А вот Международная комиссия по радиологической защите использует другое значение для этого коэффициента = 6,2 нЗв/Бк и тогда при пересчете циферка получится не такая красивая. Считать будет сложнее, представлять масштабы и т.п. Поэтому используют американские данные.

На заметку: т.е. теоретически, используя приведенную формулу может создать свой %ОВОЩ/ФРУКТ% эквивалент относительно 40K. К примеру, средний вес товарного клубня сорта (мешок которого Лукашенко подарил Путину на Новый год) составляет 100 грамм. Идем смотреть базу департамента США по сельскому хозяйству на факт содержания калия в картофеле. Важно еще выбрать правильный вариант (с кожицей/без и т.п.). Ну пусть в среднем будет 430 мг калия. Считаем и получаем 6,9 микрорентген. Выводы делайте самостоятельно (или не делайте, а читайте дальше).

Почему единица является неофициальной (и даже шуточной)? А потому что "внешний" калий (а значит и его изотопы), поступивший в организм с пищей, в нем не накапливается (т.е. "банановая доза" не является кумулятивной). Виной тому гомеостаз человеческого организма.


Гомеоста́з (др.-греч. ὁμοιοστάσις от ὅμοιος «одинаковый, подобный» + στάσις «стояние; неподвижность») — саморегуляция, способность открытой системы сохранять постоянство своего внутреннего состояния посредством скоординированных реакций, направленных на поддержание динамического равновесия.

Т.е. любой избыток компонента, поступивщий с пищей, достаточно быстро компенсируется выводом такого же количества с выделениями организма. Фактически, дополнительное облучение, вызванное употреблением банана, длится всего несколько часов после приема, то есть до тех пор, пока почки не восстановят нормальное содержание калия в организме. Говорит нам об этом и документ, выпущенный Агентством по охране окружающей среды США. Процитирую:

Для радиоизотопов элементов, активно участвующих в гомеостазе человеческого тела поправочные коэффициенты для расчета риска вдыхания или проглатывания, приведенные в этом документе, не подходят для применения в некоторых случаях. Например, коэффициент риска проглатывания для 40К не подходит для расчета при употреблении природных продуктов, содержащих повышенное содержание 40К. Это связано с тем, что биокинетическая модель, используемая в этом документе, подразумевает относительно медленное удаление этого элемента (биологический период полураспада 30 дней), что имеет место при среднестатистических объемах поступления калия в организм. Резкое же повышение употребления калия с продуктами приводит к выведению из организма равной массы биогенного калия (в том числе и изотопа 40К) за короткий период.

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

Так что… Гораздо более вредным явлением, на мой взгляд, является случай, когда множество маленьких источников излучения объединяются (в хранилищах или на складах). Недаром ходят байки о ложных срабатываниях датчиков ионизирующей радиации на таможнях США, когда через пропускной пункт проезжали машины груженые бананами.

Не знаю, многие ли в курсе, но Канада, Беларусь и Россия — являются крупнейшими производителями калийных удобрений в мире (!). Чаще всего эти удобрения идут в виде хлорида калия КCl, калимагнезии K2SO4*MgSO4 и редкой калийной селитры KNO3. А тут уже масштабы далеко не банановые. К примеру, в 1 кг самого распространенного калийного удобрения KCl (хлорид калия) ~ 524 грамма калия, т.е. это почти 1000 BED (тысячу бананов). Естественно никто в здравом уме есть это удобрение не будет, да и не сможет, т.к. довольно сильный яд. Но вот часто видел, особенно во время весенней посевной в Беларуси, мужиков, прилегших отдохнуть на мешки с удобрениями.


Грубо говоря — нашпигует электронами (распад с выделением гамма-кванта не берем в расчет) спину довольно быстро. Полиэтилен мешка не спасет. Ниже картинка для тех кто забыл уроки ГО (или у кого их попросту не было :( )


Бета-частицы (электроны) более или менее могут поглощаться только несколькими миллиметрами алюминия. Обматывайтесь фольгой, перед тем как прилечь, что ли...

В завершение, как всегда, маленькая лабораторная работа на тему "изучаем дозиметр". На картинках сравнение бананов и некоторых солей, содержащих калий.



Излучение от свежей китайской щелочи

Вот такая банка с китайским КОН (гидроксид калия). Думаю средство для прочистки труб "Крот" шпигует электронами похоже (если там используется KOH, а не более дешевый NaOH)


Дает вот такой фон


А вот такие значения у китайского же KCl (хлорид калия)


Ну и разговор про соли был бы неполным, если не упомянуть КBr (тот самый, седативный, который якобы скармливают солдатикам в козармах для уменьшения либидо), советского еще производства


Разница, как говорится, видна невооруженным глазом. Так что...

Мораль заметки: радиоактивность банана=существующая тысячелетиями радиоактивность изотопа 40К. Если вы прибыли из созвездия Сириуса (и это смогут подтвердить все догоны) с другим уровнем фоновой радиации — от бананов придется отказаться (и от беларуской картошечки, кстати, тоже), а всем остальным — "не думайте про это". Курение, кстати, вредит гораздо сильнее (по объективным причинам, вроде того, что гамма-излучение, возникающее при распаде изотопов имеющихся в табаке, проникает сильнее, чем какой-то там электрон из банана).

Cергей Бесараб (Siarhei V. Besarab)

Let's block ads! (Why?)

Небольшой обзор SIMD в .NET/C#

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

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


Немного истории

В .NET SIMD впервые появился в 2015 году с выходом .NET Framework 4.6. Тогда были добавлены типы Matrix3x2, Matrix4x4, Plane, Quaternion, Vector2, Vector3 и Vector4, которые позволили прозводить векторизированные вычисления. Позже был добавлен тип Vector<T>, который предоставил больше возможностей для векторизации алгоритмов. Но многие программисты всё равно были недовольны, т.к. вышеописанные типы ограничивали поток мыслей программиста и не давали возможности использовать полную мощь SIMD-инструкций современных процессоров. Уже в наше время, в .NET Core 3.0 Preview появилось пространство имён System.Runtime.Intrinsics, которое предоставляет гораздо большую свободу в выборе инструкций. Чтобы получить наилучшие результаты в скорости надо использовать RyuJit и нужно либо собирать под x64, либо отключить Prefer 32-bit и собирать под AnyCPU. Все бенчмарки я запускал на компьютере с процессором Intel Core i7-6700 CPU 3.40GHz (Skylake).


Суммируем элементы массива

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

Самая очевидная

public int Naive() {
    int result = 0;
    foreach (int i in Array) {
        result += i;
    }
    return result;
}

С использованием LINQ

public long LINQ() => Array.Aggregate<int, long>(0, (current, i) => current + i);

С использованием векторов из System.Numerics:

public int Vectors() {
    int vectorSize = Vector<int>.Count;
    var accVector = Vector<int>.Zero;
    int i;
    var array = Array;
    for (i = 0; i < array.Length - vectorSize; i += vectorSize) {
        var v = new Vector<int>(array, i);
        accVector = Vector.Add(accVector, v);
    }
    int result = Vector.Dot(accVector, Vector<int>.One);
    for (; i < array.Length; i++) {
        result += array[i];
    }
    return result;
}

С использованием кода из пространства System.Runtime.Intrinsics:

public unsafe int Intrinsics() {
    int vectorSize = 256 / 8 / 4;
    var accVector = Vector256<int>.Zero;
    int i;
    var array = Array;
    fixed (int* ptr = array) {
        for (i = 0; i < array.Length - vectorSize; i += vectorSize) {
            var v = Avx2.LoadVector256(ptr + i);
            accVector = Avx2.Add(accVector, v);
        }
    }
    int result = 0;
    var temp = stackalloc int[vectorSize];
    Avx2.Store(temp, accVector);
    for (int j = 0; j < vectorSize; j++) {
        result += temp[j];
    }   
    for (; i < array.Length; i++) {
        result += array[i];
    }
    return result;
}

Я запустил бенчмарк на эти 4 метода у себя на компьютере и получил такой результат:


Method ItemsCount Median
Naive 10 75.12 ns
LINQ 10 1,186.85 ns
Vectors 10 60.09 ns
Intrinsics 10 255.40 ns
Naive 100 360.56 ns
LINQ 100 2,719.24 ns
Vectors 100 60.09 ns
Intrinsics 100 345.54 ns
Naive 1000 1,847.88 ns
LINQ 1000 12,033.78 ns
Vectors 1000 240.38 ns
Intrinsics 1000 630.98 ns
Naive 10000 18,403.72 ns
LINQ 10000 102,489.96 ns
Vectors 10000 7,316.42 ns
Intrinsics 10000 3,365.25 ns
Naive 100000 176,630.67 ns
LINQ 100000 975,998.24 ns
Vectors 100000 78,828.03 ns
Intrinsics 100000 41,269.41 ns

Видно, что решения с Vectors и Intrinsics очень сильно выигрывают в скорости по сравнению с очевидным решением и с LINQ. Теперь надо разобраться что происходит в этих двух методах.

Рассмотрим подробнее метод Vectors:


Vectors
public int Vectors() {
    int vectorSize = Vector<int>.Count;
    var accVector = Vector<int>.Zero;
    int i;
    var array = Array;
    for (i = 0; i < array.Length - vectorSize; i += vectorSize) {
        var v = new Vector<int>(array, i);
        accVector = Vector.Add(accVector, v);
    }
    int result = Vector.Dot(accVector, Vector<int>.One);
    for (; i < array.Length; i++) {
        result += array[i];
    }
    return result;
}

  • int vectorSize = Vector<int>.Count; — это сколько 4х байтовых чисел мы можем поместить в вектор. Если используется аппаратное ускорение, то эта величина показывает сколько 4х-байтовых чисел можно поместить в один SIMD регистр. По сути она показывает над сколькими элементами данного типа можно проводить операции параллельно;
  • accVector — вектор, в котором будет накапливаться результат функции;
    var v = new Vector<int>(array, i); — загружаются данные в новый вектор v, из array, начиная с индекса i. Загрузится ровно vectorSize данных.
  • accVector = Vector.Add(accVector, v); — складываются два вектора.
    Например в Array хранится 8 чисел: {0, 1, 2, 3, 4, 5, 6, 7} и vectorSize == 4, тогда:
    В первой итерации цикла accVector = {0, 0, 0, 0}, v = {0, 1, 2, 3}, после сложения в accVector будет: {0, 0, 0, 0} + {0, 1, 2, 3} = {0, 1, 2, 3}.
    Во второй итерации v = {4, 5, 6, 7} и после сложения accVector = {0, 1, 2, 3} + {4, 5, 6, 7} = {4, 6, 8, 10}.
  • Остаётся только как-то получить сумму всех элементов вектора, для этого можно применить скалярное умножение на вектор, заполненный единицами: int result = Vector.Dot(accVector, Vector<int>.One);
    Тогда получится: {4, 6, 8, 10} {1, 1, 1, 1} = 4 1 + 6 1 + 8 1 + 10 * 1 = 28.
  • В конце, если требуется, то досуммируются числа, которые не помещаются в последний вектор.

Если взглянуть в код метода Intrinsics:


Intrinsics
public unsafe int Intrinsics() {
    int vectorSize = 256 / 8 / 4;
    var accVector = Vector256<int>.Zero;
    int i;
    var array = Array;
    fixed (int* ptr = array) {
        for (i = 0; i < array.Length - vectorSize; i += vectorSize) {
            var v = Avx2.LoadVector256(ptr + i);
            accVector = Avx2.Add(accVector, v);
        }
    }
    int result = 0;
    var temp = stackalloc int[vectorSize];
    Avx2.Store(temp, accVector);
    for (int j = 0; j < vectorSize; j++) {
        result += temp[j];
    }   
    for (; i < array.Length; i++) {
        result += array[i];
    }
    return result;
}

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


Сравниваем два массива

Надо сравнить два массива байт. Собственно это та задача, из-за которой я начал изучать SIMD в .NET. Напишем опять несколько методов для бенчмарка, будем сравнивать два массива: ArrayA и ArrayB:
Самое очевидное решение:

public bool Naive() {
    for (int i = 0; i < ArrayA.Length; i++) {
        if (ArrayA[i] != ArrayB[i]) return false;
    }
    return true;
}

Решение через LINQ:

public bool LINQ() => ArrayA.SequenceEqual(ArrayB);

Решение через функцию MemCmp:

[DllImport("msvcrt.dll", CallingConvention = CallingConvention.Cdecl)]
static extern int memcmp(byte[] b1, byte[] b2, long count);

public bool MemCmp() => memcmp(ArrayA, ArrayB, ArrayA.Length) == 0;

С использованием векторов из System.Numerics:

public bool Vectors() {
    int vectorSize = Vector<byte>.Count;
    int i = 0;
    for (; i < ArrayA.Length - vectorSize; i += vectorSize) {
        var va = new Vector<byte>(ArrayA, i);
        var vb = new Vector<byte>(ArrayB, i);
        if (!Vector.EqualsAll(va, vb)) {
            return false;
        }
    }
    for (; i < ArrayA.Length; i++) {
        if (ArrayA[i] != ArrayB[i])
            return false;
    }
    return true;
}

С использованием Intrinsics:

public unsafe bool Intrinsics() {
    int vectorSize = 256 / 8;
    int i = 0;
    const int equalsMask = unchecked((int) (0b1111_1111_1111_1111_1111_1111_1111_1111));
    fixed (byte* ptrA = ArrayA)
    fixed (byte* ptrB = ArrayB) {
        for (; i < ArrayA.Length - vectorSize; i += vectorSize) {
            var va = Avx2.LoadVector256(ptrA + i);
            var vb = Avx2.LoadVector256(ptrB + i);
            var areEqual = Avx2.CompareEqual(va, vb);
            if (Avx2.MoveMask(areEqual) != equalsMask) {
                return false;
            }
        }
        for (; i < ArrayA.Length; i++) {
            if (ArrayA[i] != ArrayB[i])
                return false;
        }
        return true;
    }
}

Результат бэнчмарка на моём компьютере:


Method ItemsCount Median
Naive 10000 66,719.1 ns
LINQ 10000 71,211.1 ns
Vectors 10000 3,695.8 ns
MemCmp 10000 600.9 ns
Intrinsics 10000 1,607.5 ns
Naive 100000 588,633.7 ns
LINQ 100000 651,191.3 ns
Vectors 100000 34,659.1 ns
MemCmp 100000 5,513.6 ns
Intrinsics 100000 12,078.9 ns
Naive 1000000 5,637,293.1 ns
LINQ 1000000 6,622,666.0 ns
Vectors 1000000 777,974.2 ns
MemCmp 1000000 361,704.5 ns
Intrinsics 1000000 434,252.7 ns

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

var areEqual = Avx2.CompareEqual(va, vb);
if (Avx2.MoveMask(areEqual) != equalsMask) {
    return false;
}

В первой два вектора сравниваются на равенство и результат сохраняется в вектор areEqual, у которого в элемент на конкретной позиции все биты выставляются в 1, если соответствующие элементы в va и vb равны. Получается, что если векторы из байт va и vb полностью равны, то в areEquals все элементы должны равняться 255 (11111111b). Т.к. Avx2.CompareEqual является обёрткой над _mm256_cmpeq_epi8, то на сайте Intel можно посмотреть псевдокод этой операции:
Метод MoveMask из вектора делает 32-х битное число. Значениями битов являются старшие биты каждого из 32 однобайтовых элементов вектора. Псевдокод можно посмотреть здесь.
Таким образом, если какие-то байты в va и vb не совпадают, то в areEqual соответствующие байты будут равны 0, следовательно старшие биты этих байт тоже будут равны 0, а значит и соответствующие биты в ответе Avx2.MoveMask тоже будут равны 0 и сравнение с equalsMask не пройдёт.
Разберём небольшой пример, приняв что длина вектора 8 байт (чтобы писать было меньше):


  • Пускай va = {100, 10, 20, 30, 100, 40, 50, 100}, а vb = {100, 20, 10, 30, 100, 40, 80, 90};
  • Тогда areEqual будет равен {255, 0, 0, 255, 255, 255, 0, 0};
  • Метод MoveMask вернёт 10011100b, который нужно будет сравнивать с маской 11111111b, т.к. эти маски неравны, то выходит, что и векторы va и vb неравны.

Подсчитываем сколько раз элемент встречается в коллекции

Иногда требуется посчитать сколько раз конкретный элемент встречается в коллекции, например, интов, этот алгоритм тоже можно ускорить. Напишем несколько методов для сравнения, будем искать в массиве Array элемент Item:
Самый очевидный:

public int Naive() {
    int result = 0;
    foreach (int i in Array) {
        if (i == Item) {
            result++;
        }
    }
    return result;
}

с использованием LINQ:

public int LINQ() => Array.Count(i => i == Item);

с использованием векторов из System.Numerics.Vectors:

public int Vectors() {
    var mask = new Vector<int>(Item);
    int vectorSize = Vector<int>.Count;
    var accResult = new Vector<int>();
    int i;
    var array = Array;
    for (i = 0; i < array.Length - vectorSize; i += vectorSize) {
        var v = new Vector<int>(array, i);
        var areEqual = Vector.Equals(v, mask);
        accResult = Vector.Subtract(accResult, areEqual);
    }
    int result = 0;
    for (; i < array.Length; i++) {
        if (array[i] == Item) {
            result++;
        }
    }
    result += Vector.Dot(accResult, Vector<int>.One);
    return result;
}

С использованием Intrinsics:

public unsafe int Intrinsics() {
    int vectorSize = 256 / 8 / 4;
    //var mask = Avx2.SetAllVector256(Item);
    //var mask = Avx2.SetVector256(Item, Item, Item, Item, Item, Item, Item, Item);
    var temp = stackalloc int[vectorSize];
    for (int j = 0; j < vectorSize; j++) {
        temp[j] = Item;
    }
    var mask = Avx2.LoadVector256(temp);
    var accVector = Vector256<int>.Zero;
    int i;
    var array = Array;
    fixed (int* ptr = array) {
        for (i = 0; i < array.Length - vectorSize; i += vectorSize) {
            var v = Avx2.LoadVector256(ptr + i);
            var areEqual = Avx2.CompareEqual(v, mask);
            accVector = Avx2.Subtract(accVector, areEqual);
        }
    }
    int result = 0;
    Avx2.Store(temp, accVector);
    for(int j = 0; j < vectorSize; j++) {
        result += temp[j];
    }
    for(; i < array.Length; i++) {
        if (array[i] == Item) {
            result++;
        }
    }
    return result;
}

Результат бенчмарка на моём компьютере:


Method ItemsCount Median
Naive 1000 2,824.41 ns
LINQ 1000 12,138.95 ns
Vectors 1000 961.50 ns
Intrinsics 1000 691.08 ns
Naive 10000 27,072.25 ns
LINQ 10000 113,967.87 ns
Vectors 10000 7,571.82 ns
Intrinsics 10000 4,296.71 ns
Naive 100000 361,028.46 ns
LINQ 100000 1,091,994.28 ns
Vectors 100000 82,839.29 ns
Intrinsics 100000 40,307.91 ns
Naive 1000000 1,634,175.46 ns
LINQ 1000000 6,194,257.38 ns
Vectors 1000000 583,901.29 ns
Intrinsics 1000000 413,520.38 ns

Методы Vectors и Intrinsics полностью совпадают в логике, отличия только в реализации конкретных операций. Идея в целом такая:


  • создаётся вектор mask, в котором в каждом элементе храниться искомое число;
  • Загружается в вектор v часть массива и сравнивается с маской, тогда в равных элементах в areEqual будут выставлены все биты, т.к. areEqual — вектор из интов, то, если выставить все биты одного элемента, получим -1 в этом элементе ((int)(1111_1111_1111_1111_1111_1111_1111_1111b) == -1);
  • вычитается из accVector вектор areEqual и тогда в accVector будет сумма того, сколько раз элемент item встретился во всех векторах v для каждой позиции (минус на минут дают плюс).

Весь код из статьи можно найти на GitHub


Заключение

Я рассмотрел лишь очень малую часть возможностей, которые предоставляет .NET для векторизации вычислений. За полным и актуальным список доступных интринсиков в .NETCORE под x86 можно обратиться к исходному коду. Удобно, что там в C# файлах в summary каждого интринсика есть его же название из мира C, что упрощает и понимание назначения этого интринсика, и перевод уже существующих С++/С алгоритмов на .NET. Документация по System.Numerics.Vector доступна на msdn.
На мой вгляд, у .NET есть большое преимущество перед C++, т.к. JIT компиляция происходит уже на клиентской машине, то компилятор может оптимизировать код под конкретный клиентский процессор, предоставляя максимальную производительность. При этом программист для написания быстрого кода может оставаться в рамках одного языка и технологий.

Let's block ads! (Why?)

Как Microsoft Excel работает с высотами рядов

Иногда мне бывает скучно и я, вооружившись отладчиком, начинаю копаться в разных программах. В этот раз мой выбор пал на Excel и было желание разобраться как он оперирует высотами рядов, в чём хранит, как считает высоту диапазона ячеек и т.д. Разбирал я Excel 2010 (excel.exe, 32bit, version 14.0.4756.1000, SHA1 a805cf60a5542f21001b0ea5d142d1cd0ee00b28).


Начнём с теории

Если обратиться к документации по VBA для Microsoft Office, то можно увидеть, что высоту ряда так или иначе можно получить через два свойства:


  • RowHeight — Returns or sets the height of the first row in the range specified, measured in points. Read/write Double;
  • Height — Returns a Double value that represents the height, in points, of the range. Read-only.

Причём если заглянуть ещё и сюда: Excel specifications and limits. То можно обнаружить, что максимальная высота ряда составляет 409 точек. Это к сожалению далеко не единственный случай, когда официальные документы Microsoft немножко лукавят. На самом деле в коде Excel максимальная высота ряда задана как 2047 пикселя, что в точках будет 1535.25. А максимальный размер шрифта 409.55 пунктов. Получить ряд такой огромной высоты простым присваиванием в VBA/Interop не получится, но можно взять ряд, задать его первой ячейке шрифт Cambria Math, а размер шрифта выставить в 409.55 пунктов. Тогда Excel своим хитрым алгоритмом посчитает высоту ряда на основе формата ячейки, получит число превышающее 2047 пикселей (поверьте на слово) и сам выставит у ряда максимально возможную высоту. Если спросить высоту этого ряда через UI, то Excel соврёт что высота 409.5 точек, но если запросить высоту ряда через VBA, то получится честные 1535,25 точек, что равняется 2047 пикселям. Правда после сохранения документа высота всё равно сбросится до 409,5 точек. Эту манипуляцию можно пронаблюдать вот на этом видео: http://recordit.co/ivnFEsELLI

Я не зря упомянул в предыдущем абзаце пиксели. Excel на самом деле хранит и рассчитывает размеры ячеек в целых числах (он вообще максимально всё делает в целых числах). Чаще всего это пиксели, умноженные на некоторый коэффициент. Интересно, что Excel хранит масштаб внешнего вида в виде обыкновенной дроби, например, масштаб 75% будет храниться как два числа 3 и 4. И когда надо будет вывести на экран ряд, Excel возьмёт высоту ряда как целое число пикселей, умножит на 3 и разделит на 4. Но выполнять эту операцию он будет уже в самом конце от этого создаётся эффект, что всё считается в дробных числах. Чтобы в этом убедиться напишите в VBA вот такой код:

w.Rows(1).RowHeight = 75.375
Debug.Print w.Rows(1).Height

VBA выдаст 75, т.к. 75,375 точек будет 100,5 пикселей, а Excel такое себе позволить не может и отбросит дробную часть до 100 пикселей. Когда VBA будет запрашивать высоту ряда в точках, Excel честно переведёт 100 пикселей в точки и вернёт 75.

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

class RowHeightInfo {
    public ushort Value { get; set; } //высота ряда в целых пикселях, умноженная на 4.
    public ushort Flags { get; set; } //дополнительные флаги
}

Вам пока что придётся поверить мне на слово, но в Excel высота ряда хранится именно так. Т.е., если задано, что высота ряда 75 точек, в пикселях это будет 100, то в Value будет хранится 400. Что обозначают все биты в Flags я до конца не выяснил (выяснять значения флагов сложно и долго), но знаю точно, что 0x4000 выставляется для рядов у которых высота задана вручную, а 0x2000 — выставляется у скрытых рядов. В целом у видимых рядов с заданной вручную высотой Flags чаще всего равняется 0x4005, а для рядов у которых высота высчитывается на основе форматирования Flags равняется либо 0xA, либо 0x800E.


Спрашиваем высоту ряда

Теперь в принципе можно взглянуть на метод из excel.exe, который возвращает высоту ряда по его индексу (спасибо HexRays за красивый код):

int __userpurge GetRowHeight@<eax>(signed int rowIndex@<edx>, SheetLayoutInfo *sheetLayoutInfo@<esi>, bool flag)
{
  RowHeightInfo *rowHeightInfo; // eax
  int result; // ecx

  if ( sheetLayoutInfo->dword1A0 )
    return sheetLayoutInfo->defaultFullRowHeightMul4 | (~(sheetLayoutInfo->defaultRowDelta2 >> 14 << 15) & 0x8000);
  if ( rowIndex < sheetLayoutInfo->MinRowIndexNonDefault )
    return sheetLayoutInfo->defaultFullRowHeightMul4 | (~(sheetLayoutInfo->defaultRowDelta2 >> 14 << 15) & 0x8000);
  if ( rowIndex >= sheetLayoutInfo->MaxRowIndexNonDefault )
    return sheetLayoutInfo->defaultFullRowHeightMul4 | (~(sheetLayoutInfo->defaultRowDelta2 >> 14 << 15) & 0x8000);
  rowHeightInfo = GetRowHeightCore(sheetLayoutInfo, rowIndex);
  if ( !rowHeightInfo )
    return sheetLayoutInfo->defaultFullRowHeightMul4 | (~(sheetLayoutInfo->defaultRowDelta2 >> 14 << 15) & 0x8000);
  result = 0;
  if ( flag || !(rowHeightInfo->Flags & 0x2000) )
    result = rowHeightInfo->Value;
  if ( !(rowHeightInfo->Flags & 0x4000) )
    result |= 0x8000u;
  return result;
}

Что такое dword1A0 я так и не выяснил, т.к. не смог найти место где этот флаг выставляется :(
Что такое defaultRowDelta2 для меня тоже до сих пор остаётся загадкой. Когда excel рассчитывает высоту ряда на основе формата, то представляет её как сумму двух чисел. defaultRowDelta2 — это второе число из этой суммы для стандартной высоты ряда. Значение параметра flag тоже загадочно, т.к. везде где я видел вызов этого метода в flag передавался false.
В этом методе также появляется класс SheetLayoutInfo. Я его назвал именно так, потому что в нём хранится много всякой информации о внешнем виде листа. В SheetLayoutInfo есть такие поля как:


  • DefaultFullRowHeightMul4 — стандартная высота ряда;
  • MinRowIndexNonDefault — индекс первого ряда, у которого высота отличается от стандартной;
  • MaxRowIndexNonDefault — индекс ряда, следующего за последним, у которого высота отличается от стандартной;
  • DefaultRowDelta2 — та самая часть от суммы стандартной высоты ряда.
  • GroupIndexDelta — об этом позже

В принципе логика данного метода вполне понятна:


  1. Если индекс ряда меньше первого с нестандартной высотой, то возвращаем стандартную;
  2. Если индекс ряда больше последнего с нестандартной высотой, то возвращаем стандартную;
  3. В противном случае получаем объект rowHeightInfo для ряда из метода GetRowHeightCore;
  4. Если rowHeightInfo == null возвращаем стандартную высоту ряда;
  5. Тут магия с флагами, но в общем виде мы возвращаем то, что находится в rowHeightInfo.Value и выставляем 16-й бит в ответе, если высота ряда не была задана вручную.

Если переписать этот код на C#, то получится примерно следующее:

const ulong HiddenRowMask = 0x2000;
const ulong CustomHeightMask = 0x4000;
const ushort DefaultHeightMask = 0x8000;
public static ushort GetRowHeight(int rowIndex, SheetLayoutInfo sheetLayoutInfo) {
    ushort defaultHeight = (ushort) (sheetLayoutInfo.DefaultFullRowHeightMul4 | (~(sheetLayoutInfo.DefaultRowDelta2 >> 14 << 15) & DefaultHeightMask));
    if (rowIndex < sheetLayoutInfo.MinRowIndexNonDefault)
        return defaultHeight;
    if (rowIndex >= sheetLayoutInfo.MaxRowIndexNonDefault)
        return defaultHeight;
    RowHeightInfo rowHeightInfo = GetRowHeightCore(sheetLayoutInfo, rowIndex);
    if (rowHeightInfo == null)
        return defaultHeight;
    ushort result = 0;
    if ((rowHeightInfo.Flags & HiddenRowMask) == 0)
        result = rowHeightInfo.Value;
    if ((rowHeightInfo.Flags & CustomHeightMask) == 0)
        result |= DefaultHeightMask;
    return result;
}

Теперь можно взглянуть что происходит внутри GetRowHeightCore:

RowHeightInfo *__fastcall GetRowHeightCore(SheetLayoutInfo *sheetLayoutInfo, signed int rowIndex)
{
  RowHeightInfo *result; // eax
  RowsGroupInfo *rowsGroupInfo; // ecx
  int rowInfoIndex; // edx

  result = 0;
  if ( rowIndex < sheetLayoutInfo->MinRowIndexNonDefault || rowIndex >= sheetLayoutInfo->MaxRowIndexNonDefault )
    return result;
  rowsGroupInfo = sheetLayoutInfo->RowsGroups[sheetLayoutInfo-GroupIndexDelta + (rowIndex >> 4)];
  result = 0;
  if ( !rowsGroupInfo )
    return result;
  rowInfoIndex = rowsGroupInfo->Indices[rowIndex & 0xF];
  if ( rowInfoIndex )
    result = (rowsGroupInfo + 8 * (rowInfoIndex + rowsGroupInfo->wordBA + rowsGroupInfo->wordBC - rowsGroupInfo->wordB8));
  return result;
}

  1. Опять в начале Excel проверяет находится ли индекс ряда среди рядов с изменённой высотой и если нет, то возвращает null.
  2. Находит нужную группу рядов, если такой группы нет, то возвращает null.
  3. Получает индекс ряда в группе.
  4. Далее по индексу ряда находит нужный объект класса RowHeightInfo. wordBA, wordBC, wordB8 — какие-то константы. Они изменяются только вместе с историей. В принципе на понимание алгоритма они не влияют.

Тут стоит отклониться от темы и рассказать подробнее про RowsGroupInfo. Excel хранит RowHeightInfo группами по 16 штук, где i-я группа, представленная классом RowsGroupInfo, будет хранить в себе информацию о рядах с i × 16 до i × 16 + 15 включительно.

Но информация о высоте рядов в RowsGroupInfo хранится несколько необычным способом. Скорее всего из-за необходимости поддерживать историю в Excel.

В RowsGroupInfo есть три важных поля: Indices, HeightInfos, и RowsCount, второе в коде выше не видно (оно должно быть в этой строчке: (rowsGroupInfo + 8 × (...)), т.к. rowInfoIndex может принимать очень разные значения, я видел даже больше 1000 и я понятия не имею как в IDA задать такую структуру. Поле RowsCount вообще в коде выше не встречается, но именно там хранится сколько реально нестандартных рядов хранится в группе.
Кроме того, в SheetLayoutInfo есть GroupIndexDelta — разница между реальным индексом группы и индексом первой группы с изменённой высотой ряда.

В поле Indices хранятся смещения RowHeightInfo для каждого индекса ряда внутри группы. Они хранятся там по порядку, а вот в HeightInfos RowHeightInfo уже хранятся в порядке изменения.

Допустим у нас есть новый пустой лист и мы каким-то образом изменили высоту ряда номер 23. Это ряд лежит во второй группе из 16 рядов, тогда:


  1. Excel определит индекс группы для этого ряда. В текущем случае индекс будет равен 1 и изменит GroupIndexDelta = -1.
  2. Создаст для группы рядов объект класса RowsGroupInfo и положит его в sheetLayoutInfo->RowsGroups под индексом 0 (sheetLayoutInfo->GroupIndexDelta + 1);
  3. В RowsGroupInfo Excel выделит память под 16 4-х байтовых Indices, под RowsCount, wordBA, wordBC и wordB8 и т.д..;
  4. Потом Excel вычисляет индекс ряда в группе через операцию побитового И (это сильно быстрее чем брать остаток от деления): rowIndex & 0xF. Искомый индекс в группе будет равняться: 23 & 0xF = 7;
  5. После этого Excel получает смещение для индекса 7: offset = Indices[7]. Если offset = 0, то Excel выделяет 8 байт в конце RowsGroupInto, увеличивает RowsCount на единицу и записывает новое смещение в Indices[7]. В любом случае в конце Excel запишет по смещению в RowsGroupInfo информацию о новой высоте ряда и флагах.

Сам класс RowsGroupInfo на C# выглядел бы вот так:

class RowsGroupInfo {
    public int[] Indices { get; }
    public List<RowHeightInfo> HeightInfos { get; }

    public RowsGroupInfo() {
        Indices = new int[SheetLayoutInfo.MaxRowsCountInGroup];
        HeightInfos = new List<RowHeightInfo>();
        for (int i = 0; i < SheetLayoutInfo.MaxRowsCountInGroup; i++) {
            Indices[i] = -1;
        }
    }
}

Метод GetRowHeightCore выглядел бы вот так:

static RowHeightInfo GetRowHeightCore(SheetLayoutInfo sheetLayoutInfo, int rowIndex) {
    if (rowIndex < sheetLayoutInfo.MinRowIndexNonDefault || rowIndex >= sheetLayoutInfo.MaxRowIndexNonDefault)
        return null;
    RowsGroupInfo rowsGroupInfo = sheetLayoutInfo.RowsGroups[sheetLayoutInfo.GroupIndexDelta + (rowIndex >> 4)];
    if (rowsGroupInfo == null)
        return null;
    int rowInfoIndex = rowsGroupInfo.Indices[rowIndex & 0xF];
    return rowInfoIndex != -1 ? rowsGroupInfo.HeightInfos[rowInfoIndex] : null;
}

И вот так выглядел бы SetRowHeight (его код из excel.exe я не приводил):

public static void SetRowHeight(int rowIndex, ushort newRowHeight, ushort flags, SheetLayoutInfo sheetLayoutInfo) {
    sheetLayoutInfo.MaxRowIndexNonDefault = Math.Max(sheetLayoutInfo.MaxRowIndexNonDefault, rowIndex + 1);
    sheetLayoutInfo.MinRowIndexNonDefault = Math.Min(sheetLayoutInfo.MinRowIndexNonDefault, rowIndex);
    int realGroupIndex = rowIndex >> 4;
    if (sheetLayoutInfo.RowsGroups.Count == 0) {
        sheetLayoutInfo.RowsGroups.Add(null);
        sheetLayoutInfo.GroupIndexDelta = -realGroupIndex;
    }
    else if (sheetLayoutInfo.GroupIndexDelta + realGroupIndex < 0) {
        int bucketSize = -(sheetLayoutInfo.GroupIndexDelta + realGroupIndex);
        sheetLayoutInfo.RowsGroups.InsertRange(0, new RowsGroupInfo[bucketSize]);
        sheetLayoutInfo.GroupIndexDelta = -realGroupIndex;
    }
    else if (sheetLayoutInfo.GroupIndexDelta + realGroupIndex >= sheetLayoutInfo.RowsGroups.Count) {
        int bucketSize = sheetLayoutInfo.GroupIndexDelta + realGroupIndex - sheetLayoutInfo.RowsGroups.Count + 1;
        sheetLayoutInfo.RowsGroups.AddRange(new RowsGroupInfo[bucketSize]);
    }
    RowsGroupInfo rowsGroupInfo = sheetLayoutInfo.RowsGroups[sheetLayoutInfo.GroupIndexDelta + realGroupIndex];
    if (rowsGroupInfo == null) {
        rowsGroupInfo = new RowsGroupInfo();
        sheetLayoutInfo.RowsGroups[sheetLayoutInfo.GroupIndexDelta + realGroupIndex] = rowsGroupInfo;
    }
    int rowInfoIndex = rowsGroupInfo.Indices[rowIndex & 0xF];
    RowHeightInfo rowHeightInfo;
    if (rowInfoIndex == -1) {
        rowHeightInfo = new RowHeightInfo();
        rowsGroupInfo.HeightInfos.Add(rowHeightInfo);
        rowsGroupInfo.Indices[rowIndex & 0xF] = rowsGroupInfo.HeightInfos.Count - 1;
    }
    else {
        rowHeightInfo = rowsGroupInfo.HeightInfos[rowInfoIndex];
    }
    rowHeightInfo.Value = newRowHeight;
    rowHeightInfo.Flags = flags;
}

Немного практики

После разобранного выше примера с изменением высоты ряда 23 в памяти Excel будет примерно такая картина (я задал ряду 23 высоту 75 точек):


sheetLayoutInfo
  • DefaultFullRowHeightMul4 = 80
  • DefaultRowDelta2 = 5
  • MaxRowIndexNonDefault = 24
  • MinRowIndexNonDefault = 23
  • GroupIndexDelta = -1
  • RowsGroups Count = 1
    • [0] RowsGroupInfo
    • HeightInfos Count = 1
      • [0] RowHeightInfo
      • Flags = 0x4005
      • Value = 100
    • Indices
      • [0] = -1
      • [1] = -1
      • [2] = -1
      • [3] = -1
      • [4] = -1
      • [5] = -1
      • [6] = -1
      • [7] = 0
      • [8] = -1
      • [9] = -1
      • [10] = -1
      • [11] = -1
      • [12] = -1
      • [13] = -1
      • [14] = -1
      • [15] = -1

Здесь и в следующем примере я буду выкладывать схематичное представление о том, как выглядят данные в памяти Excel, сделанное в Visual Studio из самописных классов, потому что прямой дамп из памяти не сильно информативен.
Теперь попробуем спрятать ряд 23. Для этого надо выставить бит 0x2000 у Flags. Будем изменять память на живую. Результат можно увидеть на этом видео: http://recordit.co/79vYIbwbzB.
При любом скрытии рядов Excel поступает также.
Теперь зададим высоту ряда не явно, а через формат ячейки. Пускай у ячейки A20 шрифт станет высотой 40 пунктов, тогда высота ячейки в точках станет 45,75 и в памяти Excel будет примерно такое:
sheetLayoutInfo
  • DefaultFullRowHeightMul4 = 80
  • DefaultRowDelta2 = 5
  • MaxRowIndexNonDefault = 24
  • MinRowIndexNonDefault = 20
  • GroupIndexDelta = -1
  • RowsGroups Count = 1
    • [0] RowsGroupInfo
    • HeightInfos Count = 2
      • [0] RowHeightInfo
      • Flags = 0x4005
      • Value = 100
      • [1] RowHeightInfo
      • Flags = 0x800E
      • Value = 244
    • Indices
      • [0] = -1
      • [1] = -1
      • [2] = -1
      • [3] = -1
      • [4] = 1
      • [5] = -1
      • [6] = -1
      • [7] = 0
      • [8] = -1
      • [9] = -1
      • [10] = -1
      • [11] = -1
      • [12] = -1
      • [13] = -1
      • [14] = -1
      • [15] = -1

Можно заметить, что Excel всегда хранит высоту ряда, если она не стандартная. Даже если высота не задана явно, а рассчитывается на основе содержимого ячеек или формата, то Excel всё равно посчитает её один раз и занесёт результат в соответствующую группу.


Разбираемся со вставкой/удалением рядов

Интересно было бы разобрать что происходит при вставке/удалении рядов. Соответствующий код в excel.exe найти несложно, но разбирать его не было желания, можете сами взглянуть на часть из него:


sub_305EC930

Флаг a5 определяет какая именно сейчас происходит операция.

int __userpurge sub_305EC930@<eax>(int a1@<eax>, int a2@<edx>, int a3@<ecx>, int a4, int a5, int a6)
{
  int v6; // esi
  int v7; // ebx
  int v8; // edi
  int v9; // edx
  int v10; // ecx
  size_t v11; // eax
  _WORD *v12; // ebp
  size_t v13; // eax
  size_t v14; // eax
  int v15; // eax
  unsigned __int16 *v16; // ecx
  _WORD *v17; // eax
  _WORD *v18; // ecx
  int v19; // edx
  __int16 v20; // bx
  int v21; // eax
  _WORD *v22; // ecx
  int v24; // edx
  int v25; // eax
  int v26; // esi
  int v27; // ebx
  size_t v28; // eax
  int v29; // ebp
  size_t v30; // eax
  int v31; // esi
  size_t v32; // eax
  int v33; // eax
  unsigned __int16 *v34; // ecx
  int v35; // eax
  _WORD *v36; // edx
  _WORD *v37; // ecx
  int v38; // eax
  __int16 v39; // bx
  int v40; // eax
  _WORD *v41; // ecx
  int v42; // [esp+10h] [ebp-48h]
  int v43; // [esp+10h] [ebp-48h]
  int v44; // [esp+14h] [ebp-44h]
  char v45; // [esp+14h] [ebp-44h]
  int Dst[16]; // [esp+18h] [ebp-40h]
  int v47; // [esp+5Ch] [ebp+4h]
  int v48; // [esp+60h] [ebp+8h]

  v6 = a1;
  v7 = a1 & 0xF;
  v8 = a2;
  if ( !a5 )
  {
    v24 = a4 - a1;
    v25 = a1 - a3;
    v43 = a4 - v6;
    if ( v7 >= v25 )
      v7 = v25;
    v47 = a4 - v7;
    v26 = v6 - v7;
    v27 = v7 + 1;
    v48 = v27;
    if ( !v8 )
      return v27;
    v28 = 4 * v24;
    if ( (4 * v24) > 0x40 )
      v28 = 64;
    v45 = v27 + v26;
    v29 = (v27 + v26) & 0xF;
    memmove(Dst, (v8 + 4 * v29), v28);
    v30 = 4 * v27;
    if ( (4 * v27) > 0x40 )
      v30 = 64;
    v31 = v26 & 0xF;
    memmove((v8 + 4 * (v47 & 0xF)), (v8 + 4 * v31), v30);
    v32 = 4 * v43;
    if ( (4 * v43) > 0x40 )
      v32 = 64;
    memmove((v8 + 4 * v31), Dst, v32);
    if ( !a6 )
      return v48;
    v33 = v29;
    if ( v29 < v29 + v43 )
    {
      v34 = (v8 + 4 * v29 + 214);
      do
      {
        Dst[v33++] = *v34 >> 15;
        v34 += 2;
      }
      while ( v33 < v29 + v43 );
    }
    v35 = (v45 - 1) & 0xF;
    if ( v35 >= v31 )
    {
      v36 = (v8 + 4 * ((v27 + v47 - 1) & 0xF) + 214);
      v37 = (v8 + 4 * ((v45 - 1) & 0xF) + 214);
      v38 = v35 - v31 + 1;
      do
      {
        v39 = *v37 ^ (*v37 ^ *v36) & 0x7FFF;
        v37 -= 2;
        *v36 = v39;
        v36 -= 2;
        --v38;
      }
      while ( v38 );
      v27 = v48;
    }
    v40 = v31;
    if ( v31 >= v31 + v43 )
      return v27;
    v41 = (v8 + 4 * v31 + 214);
    do
    {
      *v41 = *v41 & 0x7FFF | (LOWORD(Dst[v40++]) << 15);
      v41 += 2;
    }
    while ( v40 < v31 + v43 );
    return v27;
  }
  v9 = a1 - a4;
  v10 = a3 - a1;
  v42 = a1 - a4;
  v48 = 16 - v7;
  if ( 16 - v7 >= v10 )
    v48 = v10;
  if ( !v8 )
    return v48;
  v11 = 4 * v9;
  if ( (4 * v9) > 0x40 )
    v11 = 64;
  v12 = (v8 + 4 * (a4 & 0xF));
  v44 = a4 & 0xF;
  memmove(Dst, v12, v11);
  v13 = 4 * v48;
  if ( (4 * v48) > 0x40 )
    v13 = 64;
  memmove(v12, (v8 + 4 * v7), v13);
  v14 = 4 * v42;
  if ( (4 * v42) > 0x40 )
    v14 = 64;
  memmove((v8 + 4 * ((a4 + v48) & 0xF)), Dst, v14);
  if ( !a6 )
    return v48;
  v15 = a4 & 0xF;
  if ( v44 < v44 + v42 )
  {
    v16 = (v8 + 4 * v44 + 214);
    do
    {
      Dst[v15++] = *v16 >> 15;
      v16 += 2;
    }
    while ( v15 < v44 + v42 );
  }
  if ( v7 < v48 + v7 )
  {
    v17 = (v8 + 4 * v7 + 214);
    v18 = v12 + 107;
    v19 = v48;
    do
    {
      v20 = *v17 ^ (*v17 ^ *v18) & 0x7FFF;
      v17 += 2;
      *v18 = v20;
      v18 += 2;
      --v19;
    }
    while ( v19 );
  }
  v21 = a4 & 0xF;
  if ( v44 >= v44 + v42 )
    return v48;
  v22 = (v8 + 4 * (v44 + v48) + 214);
  do
  {
    *v22 = *v22 & 0x7FFF | (LOWORD(Dst[v21++]) << 15);
    v22 += 2;
  }
  while ( v21 < v44 + v42 );
  return v48;
}

К тому же по внешнему виду можно примерно понять что там происходит, а остальное добить по косвенным признакам.
Попытаемся обозначить эти косвенные признаки. Сначала зададим высоту для рядов с 16 по 64 включительно в случайном порядке. Потом перед рядом под индексом 39 вставим новый ряд. Новый ряд будет копировать высоту у ряда 38.
Посмотрим на информацию в группах рядов до и после добавления ряда, я выделил жирным различия:


До добавления ряда После добавления ряда
Смещения в первой группе: Смещения в первой группе:
0E, 04, 07, 00, 05, 0A, 09, 0F, 03, 06, 08, 0D, 01, 0B, 0C, 02 0E, 04, 07, 00, 05, 0A, 09, 0F, 03, 06, 08, 0D, 01, 0B, 0C, 02
Значения высот рядов в первой группе: Значения высот рядов в первой группе:
05, 2B, 35, 45, 4B, 50, 5B, 6B, 7B, 8B, A5, AB, B0, B5, E0, 100 05, 2B, 35, 45, 4B, 50, 5B, 6B, 7B, 8B, A5, AB, B0, B5, E0, 100
Смещения во второй группе: Смещения во второй группе:
06, 02, 0E, 09, 01, 07, 0F, 0C, 00, 0A, 04, 0B, 03, 08, 0D, 05 06, 02, 0E, 09, 01, 07, 0F, 05, 0C, 00, 0A, 04, 0B, 03, 08, 0D
Значения высот рядов во второй группе: Значения высот рядов во второй группе:
10, 15, 20, 25, 30, 75, 85, 90, 9B, A0, C5, CB, D0, D5, E5, F0 10, 15, 20, 25, 30, F0, 85, 90, 9B, A0, C5, CB, D0, D5, E5, F0
Смещения в третьей группе: Смещения в третьей группе:
0C, 08, 0E, 07, 0A, 01, 06, 0F, 09, 0D, 00, 05, 0B, 02, 04, 03 03, 0C, 08, 0E, 07, 0A, 01, 06, 0F, 09, 0D, 00, 05, 0B, 02, 04
Значения высот рядов в третьей группе: Значения высот рядов в третьей группе:
0B, 1B, 3B, 40, 55, 60, 65, 70, 80, 95, BB, C0, DB, EB, F5, FB 0B, 1B, 3B, 75, 55, 60, 65, 70, 80, 95, BB, C0, DB, EB, F5, FB
Смещения в четвёртой группе: Смещения в четвёртой группе:
_ 00
Значения высот рядов в четвёртой группе: Значения высот рядов в четвёртой группе:
_ 40

Получается то, что и ожидалось: Excel вставляет во вторую группу новый ряд с индексом 7 (39 & 0xF), у которого смещение равняется 0x05, копирует высоту ряда у индекса 6, при этом последний ряд, который был со смещением 05, выталкивается в следующую группу, а оттуда последний ряд выталкивается в четвёртую и т.д.

Теперь посмотрим, что происходит если удалить 29-й ряд.


До удаления ряда После удаления ряда
Смещения в первой группе: Смещения в первой группе:
0E, 04, 07, 00, 05, 0A, 09, 0F, 03, 06, 08, 0D, 01, 0B, 0C, 02 0E, 04, 07, 00, 05, 0A, 09, 0F, 03, 06, 08, 0D, 01, 0C, 02, 0B
Значения высот рядов в первой группе: Значения высот рядов в первой группе:
05, 2B, 35, 45, 4B, 50, 5B, 6B, 7B, 8B, A5, AB, B0, B5, E0, 100 05, 2B, 35, 45, 4B, 50, 5B, 6B, 7B, 8B, A5, 85, B0, B5, E0, 100
Смещения во второй группе: Смещения во второй группе:
06, 02, 0E, 09, 01, 07, 0F, 05, 0C, 00, 0A, 04, 0B, 03, 08, 0D 02, 0E, 09, 01, 07, 0F, 05, 0C, 00, 0A, 04, 0B, 03, 08, 0D, 06
Значения высот рядов во второй группе: Значения высот рядов во второй группе:
10, 15, 20, 25, 30, F0, 85, 90, 9B, A0, C5, CB, D0, D5, E5, F0 10, 15, 20, 25, 30, F0, 75, 90, 9B, A0, C5, CB, D0, D5, E5, F0
Смещения в третьей группе: Смещения в третьей группе:
03, 0C, 08, 0E, 07, 0A, 01, 06, 0F, 09, 0D, 00, 05, 0B, 02, 04 0C, 08, 0E, 07, 0A, 01, 06, 0F, 09, 0D, 00, 05, 0B, 02, 04, 03
Значения высот рядов в третьей группе: Значения высот рядов в третьей группе:
0B, 1B, 3B, 75, 55, 60, 65, 70, 80, 95, BB, C0, DB, EB, F5, FB 0B, 1B, 3B, 40, 55, 60, 65, 70, 80, 95, BB, C0, DB, EB, F5, FB
Смещения в четвёртой группе: Смещения в четвёртой группе:
00 00
Значения высот рядов в четвёртой группе: Значения высот рядов в четвёртой группе:
40 50

В принципе при удалении ряда происходят операции обратные вставке. При этом четвёртая группа продолжает существовать и значение высоты ряда там заполняется стандартной высотой с соответствующим флагом — 0x8005.

Этих данных достаточно для того, чтобы воспроизвести этот алгоритм на C#:


InsertRow
public static void InsertRow(SheetLayoutInfo sheetLayoutInfo, int rowIndex) {
    if (rowIndex >= sheetLayoutInfo.MaxRowIndexNonDefault)
        return;
    RowHeightInfo etalonRowHeightInfo = GetRowHeightCore(sheetLayoutInfo, rowIndex);
    RowHeightInfo newRowHeightInfo = etalonRowHeightInfo != null ? etalonRowHeightInfo.Clone() : CreateDefaultRowHeight(sheetLayoutInfo);
    int realGroupIndex = (rowIndex + 1) >> 4;
    int newRowInGroupIndex = (rowIndex + 1) & 0xF;
    int groupIndex;
    for (groupIndex = realGroupIndex + sheetLayoutInfo.GroupIndexDelta; groupIndex < sheetLayoutInfo.RowsGroups.Count; groupIndex++, newRowInGroupIndex = 0) {
        if (groupIndex < 0)
            continue;
        if (groupIndex == SheetLayoutInfo.MaxGroupsCount)
            break;
        RowsGroupInfo rowsGroupInfo = sheetLayoutInfo.RowsGroups[groupIndex];
        if (rowsGroupInfo == null) {
            if ((newRowHeightInfo.Flags & CustomHeightMask) == 0)
                continue;
            rowsGroupInfo = new RowsGroupInfo();
            sheetLayoutInfo.RowsGroups[groupIndex] = rowsGroupInfo;
        }
        int rowInfoIndex = rowsGroupInfo.Indices[newRowInGroupIndex];
        RowHeightInfo lastRowHeightInGroup;
        if (rowInfoIndex == -1 || rowsGroupInfo.HeightInfos.Count < SheetLayoutInfo.MaxRowsCountInGroup) {
            lastRowHeightInGroup = GetRowHeightCore(sheetLayoutInfo, ((groupIndex - sheetLayoutInfo.GroupIndexDelta) << 4) + SheetLayoutInfo.MaxRowsCountInGroup - 1);
            Array.Copy(rowsGroupInfo.Indices, newRowInGroupIndex, rowsGroupInfo.Indices, newRowInGroupIndex + 1, SheetLayoutInfo.MaxRowsCountInGroup - 1 - newRowInGroupIndex);
            rowsGroupInfo.HeightInfos.Add(newRowHeightInfo);
            rowsGroupInfo.Indices[newRowInGroupIndex] = rowsGroupInfo.HeightInfos.Count - 1;
        }
        else {
            int lastIndex = rowsGroupInfo.Indices[SheetLayoutInfo.MaxRowsCountInGroup - 1];
            lastRowHeightInGroup = rowsGroupInfo.HeightInfos[lastIndex];
            Array.Copy(rowsGroupInfo.Indices, newRowInGroupIndex, rowsGroupInfo.Indices, newRowInGroupIndex + 1, SheetLayoutInfo.MaxRowsCountInGroup - 1 - newRowInGroupIndex);
            rowsGroupInfo.HeightInfos[lastIndex] = newRowHeightInfo;
            rowsGroupInfo.Indices[newRowInGroupIndex] = lastIndex;
        }
        newRowHeightInfo = lastRowHeightInGroup ?? CreateDefaultRowHeight(sheetLayoutInfo);
    }
    if ((newRowHeightInfo.Flags & CustomHeightMask) != 0 && groupIndex != SheetLayoutInfo.MaxGroupsCount) {
        SetRowHeight(((groupIndex - sheetLayoutInfo.GroupIndexDelta) << 4) + newRowInGroupIndex, newRowHeightInfo.Value, newRowHeightInfo.Flags, sheetLayoutInfo);
    }
    else {
        sheetLayoutInfo.MaxRowIndexNonDefault = Math.Min(sheetLayoutInfo.MaxRowIndexNonDefault + 1, SheetLayoutInfo.MaxRowsCount);
    }
}

RemoveRow
public static void RemoveRow(SheetLayoutInfo sheetLayoutInfo, int rowIndex) {
    if (rowIndex >= sheetLayoutInfo.MaxRowIndexNonDefault)
        return;
    int realGroupIndex = rowIndex >> 4;
    int newRowInGroupIndex = rowIndex & 0xF;
    int groupIndex;
    for (groupIndex = realGroupIndex + sheetLayoutInfo.GroupIndexDelta; groupIndex < sheetLayoutInfo.RowsGroups.Count; groupIndex++, newRowInGroupIndex = 0) {
        if (groupIndex < -1)
            continue;
        if (groupIndex == -1) {
            sheetLayoutInfo.RowsGroups.Insert(0, null);
            sheetLayoutInfo.GroupIndexDelta++;
            groupIndex = 0;
        }
        if (groupIndex == SheetLayoutInfo.MaxGroupsCount)
            break;
        var newRowHeightInfo = groupIndex == SheetLayoutInfo.MaxGroupsCount - 1 ? null : GetRowHeightCore(sheetLayoutInfo, (groupIndex - sheetLayoutInfo.GroupIndexDelta + 1) << 4);
        RowsGroupInfo rowsGroupInfo = sheetLayoutInfo.RowsGroups[groupIndex];
        if (rowsGroupInfo == null) {
            if (newRowHeightInfo == null || (newRowHeightInfo.Flags & CustomHeightMask) == 0)
                continue;
            rowsGroupInfo = new RowsGroupInfo();
            sheetLayoutInfo.RowsGroups[groupIndex] = rowsGroupInfo;
        }
        if (newRowHeightInfo == null) {
            newRowHeightInfo = CreateDefaultRowHeight(sheetLayoutInfo);
        }
        int rowInfoIndex = rowsGroupInfo.Indices[newRowInGroupIndex];
        if (rowInfoIndex == -1) {
            for (int i = newRowInGroupIndex; i < SheetLayoutInfo.MaxRowsCountInGroup - 1; i++) {
                rowsGroupInfo.Indices[i] = rowsGroupInfo.Indices[i + 1];
            }
            rowsGroupInfo.HeightInfos.Add(newRowHeightInfo);
            rowsGroupInfo.Indices[SheetLayoutInfo.MaxRowsCountInGroup - 1] = rowsGroupInfo.HeightInfos.Count - 1;
        }
        else {
            for(int i = newRowInGroupIndex; i < rowsGroupInfo.HeightInfos.Count - 1; i++) {
                rowsGroupInfo.Indices[i] = rowsGroupInfo.Indices[i + 1];
            }
            rowsGroupInfo.Indices[rowsGroupInfo.HeightInfos.Count - 1] = rowInfoIndex;
            rowsGroupInfo.HeightInfos[rowInfoIndex] = newRowHeightInfo;
        }
    }
    if(rowIndex <= sheetLayoutInfo.MinRowIndexNonDefault) {
        sheetLayoutInfo.MinRowIndexNonDefault = Math.Max(sheetLayoutInfo.MinRowIndexNonDefault - 1, 0);
    }
}

Весь вышеописанный код вы можете найти на GitHub


Выводы

Код Excel уже не в первый раз удивляет интересными приёмами. В этот раз я узнал как он хранит информацию о высотах рядов. Если сообществу будет интересно, то в следующей статье покажу как Excel считает высоту диапазона ячеек (спойлер: там что-то похожее на SQRT-декомпозицию, но почему-то без кеширования сумм), там же можно будет увидеть как он применяет масштабирование в целых числах.

Let's block ads! (Why?)

Крупнейшие фирмы Уолл-стрит договорились запустить новую биржу для конкуренции с Nasdaq и NYSE

Изображение: Unsplash

В понедельник, 7 января, консорциум, объединивший одни из крупнейших инвестиционных компаний США, объявил о планах по запуску новой биржи. Площадка с предполагаемым названием MEMX (сокр. от Members Exchange) должна статья конкурентом знаменитых Nasdaq и NYSE, предложив более выгодные условия для торговли.

Зачем нужна новая биржа


Брокеры и трейдеры с Уолл-стрит на протяжение многих лет высказывали недовольство высокими комиссиями, которые приходилось платить крупнейшим американским биржам. Теперь компании Morgan Stanley, Fidelity Investments и Citadel Securities LLC решили изменить сложившуюся ситуацию.

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

Всего в консорциум учредителей новой биржи войдут девять компаний, включая Bank of America Merrill Lynch, Charles Schwab Corp, E*TRADE Financial Corp, TD Ameritrade Holdings Corp, UBS и Virtu Financial.

Перспективы MEMX


Основатели новой биржи планируют привлечь трейдеров более низкими комиссиями и тарифами на доступ к рыночным данным, а также высокой скоростью работы торговой системы (она будет построена на наиболее актуальных технологиях).

В прошлом году внимание на слишком высокие сборы бирж обращала в том числе американская комиссия по ценным бумагам и биржам (SEC). Кроме того, регулятор отменил повышение цен на доступ к данным, которое площадки осуществили в мае 2018 года.

Администрация MEMX планирует подать заявку на получение лицензии в SEC в начале 2019 года.

Другие материалы по теме финансов и фондового рынка от ITI Capital:


Let's block ads! (Why?)

Теория счастья. Статистика, как научный способ чего-либо не знать

[Перевод] Курс MIT «Безопасность компьютерных систем». Лекция 23: «Экономика безопасности», часть 2

Массачусетский Технологический институт. Курс лекций #6.858. «Безопасность компьютерных систем». Николай Зельдович, Джеймс Микенс. 2014 год


Computer Systems Security — это курс о разработке и внедрении защищенных компьютерных систем. Лекции охватывают модели угроз, атаки, которые ставят под угрозу безопасность, и методы обеспечения безопасности на основе последних научных работ. Темы включают в себя безопасность операционной системы (ОС), возможности, управление потоками информации, языковую безопасность, сетевые протоколы, аппаратную защиту и безопасность в веб-приложениях.

Лекция 1: «Вступление: модели угроз» Часть 1 / Часть 2 / Часть 3
Лекция 2: «Контроль хакерских атак» Часть 1 / Часть 2 / Часть 3
Лекция 3: «Переполнение буфера: эксплойты и защита» Часть 1 / Часть 2 / Часть 3
Лекция 4: «Разделение привилегий» Часть 1 / Часть 2 / Часть 3
Лекция 5: «Откуда берутся ошибки систем безопасности» Часть 1 / Часть 2
Лекция 6: «Возможности» Часть 1 / Часть 2 / Часть 3
Лекция 7: «Песочница Native Client» Часть 1 / Часть 2 / Часть 3
Лекция 8: «Модель сетевой безопасности» Часть 1 / Часть 2 / Часть 3
Лекция 9: «Безопасность Web-приложений» Часть 1 / Часть 2 / Часть 3
Лекция 10: «Символьное выполнение» Часть 1 / Часть 2 / Часть 3
Лекция 11: «Язык программирования Ur/Web» Часть 1 / Часть 2 / Часть 3
Лекция 12: «Сетевая безопасность» Часть 1 / Часть 2 / Часть 3
Лекция 13: «Сетевые протоколы» Часть 1 / Часть 2 / Часть 3
Лекция 14: «SSL и HTTPS» Часть 1 / Часть 2 / Часть 3
Лекция 15: «Медицинское программное обеспечение» Часть 1 / Часть 2 / Часть 3
Лекция 16: «Атаки через побочный канал» Часть 1 / Часть 2 / Часть 3
Лекция 17: «Аутентификация пользователя» Часть 1 / Часть 2 / Часть 3
Лекция 18: «Частный просмотр интернета» Часть 1 / Часть 2 / Часть 3
Лекция 19: «Анонимные сети» Часть 1 / Часть 2 / Часть 3
Лекция 20: «Безопасность мобильных телефонов» Часть 1 / Часть 2 / Часть 3
Лекция 21: «Отслеживание данных» Часть 1 / Часть 2 / Часть 3
Лекция 22: «Информационная безопасность MIT» Часть 1 / Часть 2 / Часть 3
Лекция 23: «Экономика безопасности» Часть 1 / Часть 2

Аудитория: как же спамеры работают со списками рассылки, особенно с огромными списками?

Профессор: с агрегацией списков рассылки существуют проблемы, так как очень сложно обеспечить массовую рассылку. Возможно, спамерам необходимо использовать эвристику, с помощью которой они масштабируют платёж в соответствии с величиной списка. Например, эвристически разумнее будет отправить письма 1000, а не 350 миллионам человек или что-то вроде того. Но вы правы в том, что для списков рассылок существуют практические ограничения.
Итак, что может сделать спамер, чтобы обойти упомянутые выше способы защиты от спама? У злоумышленников имеются три обходных пути.

Первый – это сеть ботнет со множеством IP-адресов, которые спамер может использовать. Даже если кто-то попытается составить «черный список» IP-адресов, злоумышленник может перебрать кучу IP-адресов в сети ботнет и обойти фильтрацию «черного списка».

Второе – это использование для рассылки спама взломанных учетных записей электронной почты. Это очень выгодный способ, потому что в силу высочайшей популярности почтовые сервисы Gmail, Yahoo или Hotmail не могут быть помещены в «чёрный список». Если вы занесли в такой список весь сервис, значит, вы закрыли его для десятков миллионов людей.

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

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

Третий обходной путь – это захват IP-адреса у законного владельца. Как упоминал Марк на предыдущей лекции, существует сетевой протокол под названием BGP, который используется для управления маршрутизацией в интернете. Таким образом, есть атаки, в которых хакер заявляет, что он выступает владельцем IP-адресов, хотя на самом деле он ими не владеет. Из-за этого весь трафик, связанный с этими адресами, будет поступать атакующему, и он сможет использовать эти IP-адреса для рассылки спама. Как только спамера засекут, он перестанет использовать BGP для одной автономной системы и переключится на другую.

Существует множество исследований на тему того, как осуществлять аутентификацию BGP, чтобы предотвратить захват IP-адреса, и куча разных защитных методов, которые злоумышленники могут попытаться обойти. Но все эти способы обхода не бесплатны, потому что злоумышленник должен каким-то образом заплатить за ботнет или за то, чтобы попасть внутрь аккаунтов электронной почты. Так что любое из защитных мероприятий повысит себестоимость генерации спама. Поэтому эти защитные меры полезны, хотя и не идеальны.

Так как же выглядят сети ботнет? В общем случае у нас имеется облако, в котором расположена инфраструктура Command & Control, отдающая команды всем подчинённым ботам. Итак, спамер обращается к C&C и говорит: «вот мои новые спам-сообщения, которые я хочу отправить», после чего боты начинают действовать от имени инфраструктуры командования и управления и отправлять сообщения группе людей.

Чем же полезны боты? Как я уже упоминал, у них есть IP-адреса, они обладают пропускной способностью, совершают вычислительные циклы, иногда эти боты сами используются в качестве веб-сервера. Так что эти вещи очень и очень полезны для спамера, к тому же служат неким слоем косвенной адресации. Косвенная адресация очень полезна для злоумышленников. Это означает, что если правоохранительные органы или кто-либо ещё отключат этот слой, не затрагивая инфраструктуру самого C&C, то спамер может просто присоединить командно-контрольную инфраструктуру к другому набору ботов и продолжать своё дело.

Такова одна из причин полезности ботов. Сеть ботнет может масштабироваться до миллионов IP-адресов, так что люди будут кликать по случайным ссылкам, всё время задействуя вредоносное программное обеспечение. Так что эти штуки могут стать очень и очень большими. Поскольку в сети ботнет вовлечены целые компании, в них могут присутствовать миллионы и миллионы компьютеров, поэтому эти сети достаточно сложны в техническом отношении.

Так сколько стоит установить вредоносные программы для всех этих ботов? Нужно помнить, что, как правило, это обычные компьютеры конечных пользователей. Стоимость размещения вредоносных программ на одном из компьютеров, или цена за один хост, составляет около 10 центов для американских хостов и порядка одного цента для азиатских хостов. Существует несколько причин того, почему цена настолько разнится. Возможно, люди склонны думать, что связь, установленная из США, заслуживает большего доверия. В то же время азиатские компьютеры в большей степени используют пиратское ПО, которое не обновляется пакетами безопасности, поэтому организовать сеть ботнет в Азии намного дешевле.

Вы увидите некоторые очень интересные статистические данные о том, как могут колебаться эти затраты, потому что такие компании, как Microsoft, пытаются искоренить пиратство. Но в любом случае, это приблизительная оценка. Достаточно сказать, что это не слишком дорого.

Что же делает этот центр C&C и как он выглядит? В простейшем виде это централизованная компьютерная система из одной или нескольких машин. Злоумышленник просто работает на этих машинах, отправляя оттуда команды для сети ботнет. Поскольку это централизованная система, злоумышленнику будет очень полезно иметь то, что известно как «пуленепробиваемый хостинг». Его идея заключается в том, что вы размещаете инфраструктуру Command & Control на серверах интернет-провайдеров, которые игнорируют запросы финансовых или правоохранительных органов на отключение таких серверов. «Пуленепробиваемые серверы» действительно существуют.

Они обходятся дороже, потому что в таком деле присутствуют риски, но если вы сможете разместить там свой центр C&C, это будет большой удачей. Потому что когда американское правительство или банк Goldman Sachs говорит такому провайдеру: «эй, отключи этого парня, который рассылает спам!», он отвечает: «каким образом вы можете меня заставить это сделать? Я работаю в другой юрисдикции и не обязан соблюдать законы об интеллектуальной собственности». Как я уже сказал, эти типы хостов фактически взимают премию за риск запуска подобных сервисов на своих серверах.

Другой альтернативой запуска инфраструктуры C&C является одноранговая сеть P2P, представляющая собой мини-ботнет. Здесь вся инфраструктура управления распределена по различным компьютерам, и в любой момент времени имеется другой компьютер, который берёт на себя роль C&C, отдавая команды всем этим рабочим узлам. Это хорошо тем, что не требует доступа к одному из этих «пуленепробиваемых хостов». Вы можете построить инфраструктуру C&C, используя обычных ботов. P2P затрудняет предоставление гарантий доступности хостов, расположенных в этом облаке, зато у него есть и другие преимущества. В целом, это два подхода, которые могут использовать злоумышленники для рассылки спама.

Так что же произойдет, если хостинг будет закрыт? Существует пара вещей, которые в таком случае может сделать спамер. Например, он может использовать DNS для перенаправления запросов. Предположим, что кто-то начинает отключать серверы, противодействуя спамеру. Но пока серверы еще живы, злоумышленник создает списки IP-адресов сервера, в которых могут быть сотни или тысячи этих адресов. После этого он начнёт привязывать каждый адрес к имени хоста на очень короткий период времени, скажем, на 300 секунд. Это позволяет злоумышленнику бороться м последствиями отключения серверов, которые на основе эвристики считаются распространителями спам-сообщений. По сути, каждые 300 секунд он меняет место рассылки спама. Так что косвенная адресация является отличной перспективой для спамера. Как я уже сказал, использование косвенной адресации является для спамера основным способом уклонения от правоохранительных органов и эвристических методов защиты.

Можно задаться вопросом, что будет, если мы просто уничтожим DNS-сервер спамера? Насколько сложно это сделать? В лекционной статье указано, что существует несколько уровней, на которых вы можете контратаковать спамера. Например, вы можете попытаться удалить регистрацию домена злоумышленника. Например, вы говорите: «эй, если вы ищете russianpharma.rx.biz.org, то переходите вот на этот DNS-сервер и общайтесь через него!». То есть как только кто-то пытается попасть на DNS-сервер спамера, вы перенаправляете его на домен верхнего уровня. Однако сложность представляет то, что злоумышленник может использовать методы быстрого переключения потока на другом уровне. Например, он может «прокручивать» серверы, которые используются в качестве спамерских DNS-серверов, то есть переключаться между серверами, которые он применяет для рассылки спама, и так далее и тому подобное. Таким образом, мы видим, как эти люди могут использовать несколько машин, чтобы попытаться избежать обнаружения.

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

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

Для обнаружения такого типа спама они используют эвристику. Они могут попытаться использовать капчу. Если они подозревают, что вы отправили 5 спам-сообщений подряд, то могут попросить вас ввести цифры с одной из этих размытых картинок или нечто подобное.
Однако многие из этих методов работают не очень хорошо. Если вы посмотрите на цену взломанного аккаунта, то вам, как спамеру, она покажется достаточно дешёвой – от одного до 5 центов за учетную запись Yahoo, Gmail или Hotmail. Это очень и очень дешево. Так что подобная защита не способна заставить спамеров отказаться от покупки взломанных аккаунтов. Это немного разочаровывает, потому что кажется, что куда бы мы не зашли, мы должны решать капчу, если хотим что-то купить или отправить почту. Так что же случилось с капчей, ведь она должна была предотвращать зловредные вещи?

Как выясняется, злоумышленник может создавать сервисы для решения капчи, и процесс ввода капчи может быть автоматизирован, как и все остальное. Как оказалось, стоимость решения одной капчи – примерно 0,001 доллара, причем это может быть сделано с очень низкой задержкой. Поэтому капча не является серьёзным барьером против спама. Вы можете подумать, что капчи решаются компьютерами, программным обеспечением. Но на самом деле, это не так, в большинстве случаев капчи решают реальные люди, и злоумышленник может передать это дело на аутсорсинг двумя способами.

В первую очередь хакер может просто найти рынок труда с очень дешёвой рабсилой и использовать людей в качестве решателей капчи. Например, спамер озабочен капчей Gmail, в таком случае он пересылает её туда, где сидит человек, тот решает её для спамера за небольшую сумму денег, а затем спамер отправляет ответ на легальный сайт. Вы также можете сделать это с помощью «механического турка» Mechanical Turk. Ребята, вы слышали о «механическом турке»?

Он довольно изящен, я имею ввиду «изящен» как способ творить зло. Вы можете разместить эти задачи на сайте Mechanical Turk и сказать: «эй, я просто играю в решение задачек с картинками», или что-то вроде этого. Или вы можете открыто заявить, что у вас есть капчи, которые нужно решить. Вы публикуете цену, после чего рынок сводит вас с людьми, которые готовы выполнить эту задачу, они сообщают ответы, и вы их публикуете. Так можно автоматизировать трудовой процесс спамера, однако нужно учесть, что Amazon, которому принадлежит Mechanical Turk, взимает плату за его использование.

Второй способ аутсорсинга заключается в том, что спамер берёт капчу с нужного ему сайта и копирует её на легальном сайте, куда переадресовывает пользователя. Тот решает капчу фактически за злоумышленника, который затем вводит правильный ответ на своём сайте, пользуясь плодами труда ничего не подозревающего пользователя. Кроме того, если спамер не доверяет своим пользователям, он может продублировать работу, используя краудсорсинг.

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

По вышеуказанным причинам капча работает не настолько хорошо, как можно подумать. Поэтому провайдеры Gmail или Yahoo пытаются использовать капчи как можно чаще, чтобы усложнить спамерам жизнь. Однако проблема состоит в том, что частое использование капчи в первую очередь раздражает добропорядочных пользователей.

Хорошим примером усложнения процесса генерации спама является двухфакторная аутентификация Gmail. Вообще-то, это очень хорошая идея. Если Gmail определит, что вы пытаетесь войти в Gmail-аккаунт с компьютера, который он не знает, то отправит вам на телефон текстовое сообщение, говорящее: «введите этот код верификации, прежде чем продолжить пользоваться нашим сервисом».

Забавно, что это отличная идея, но, по крайней мере, меня это очень раздражает. Я понимаю, что это сделано для моей же безопасности, но всё равно злюсь. Если я не часто пользуюсь разными компьютерами, то согласен на такие условия, в противном случае это становится очень неудобным.

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

Аудитория: получается, что одной из причин того, что зашифрованные электронные письма не получили широкого признания, является большая роль фильтров спама?

Профессор: вы имеете в виду, что тогда фильтры не смогут просмотреть содержание письма и не поймут, что происходит? Это хороший вопрос. На самом деле на него трудно ответить, эта как вечная проблема, что было раньше – яйцо или курица.

Поскольку не наблюдается огромного объема зашифрованной электронной почты, неясно, могли бы спамеры извлечь из этого выгоду. Но я не вижу, что это может быть проблемой. Я имею в виду, что люди изучили способы выполнения вычислений с использованием зашифрованных данных. Так что, возможно, спамеров это бы не остановило. Например, существуют фильтры спама, основанные на модели Маркова. Так что же придумали спамеры? Они начали создавать изображения, на которые не реагируют сканеры текста, и размещать внутри них спам-контент. Так что это всегда гонка вооружений.

Итак, давайте перейдем к поддержке кликов. Что же это означает? Если рекламный трюк удался и пользователю дается ссылка, то после клика по ней пользователь связывается с неким DNS-сервером, который переводит имя хоста, содержащееся в этой ссылке, в IP-адрес. После этого пользователь должен связаться с каким-то веб-сервером, который имеет этот IP-адрес. Чтобы все это сработало, спамер должен зарегистрировать доменное имя, запустить DNS-сервер и веб-сервер. Получается, что спамер должен много чего сделать, чтобы эта поддержка кликов заработала.

Можно спросить, почему бы спамеру просто не использовать «сырые» IP-адреса, как, например, в этих спам-URL? У кого-нибудь есть мысли по этому поводу? Почему бы вам просто не использовать что-то типа 183.4.4… вместо russianjewels.biz?

Аудитория: потому что это выглядит более понятным и осмысленным.

Профессор: правильно, потому что если пользователь посмотрит на эту вещь, которая представляет собой просто кучу цифр, она покажется ему странной. Как выясняется, это отсеивает лишь немногих пользователей, но вы совершенно правы. Есть группа людей, которых спамер мог бы потерять из-за того, что они не захотели бы кликать по ссылке из кучи непонятных цифр. Другая причина заключается в том, что DNS — инфраструктуры таково рода предоставляют атакующему другой уровень косвенности. Если правоохранители закрыли домен нижнего уровня инфраструктуры DNS, не затронув основной веб-сервер, то атакующий может использовать доменное имя более высокого уровня со старым сервером. Это одна из причин, по которой злоумышленники обычно не используют «сырой» IP-адрес в качестве спам-адреса.

Примером того, как в игру вступает косвенная адресация, служит то, что эти спам-URL часто указывают на сайты перенаправления, такие, как сервис bit.ly. Кроме того, роль редирект-страницы зачастую играет взломанный сайт. Злоумышленнику достаточно установить там соответствующий HTML или JavaScript, чтобы при входе на этой сайт браузер перенаправлял пользователя на какой-то другой сайт. Это полезно ещё и потому, что обеспечивает некий уровень косвенности. Фактически редирект действует как умножитель спама, потому что, имея в тылу один спамерский веб-сервер, на переднем плане можно разместить несколько различных спамерских сайтов.

Возможно, что это позволит спамеру запутать фильтры, которые занесут в «черный список» 10% ваших URL, оставив рабочими остальные 90%. Поэтому технология редиректа используется спамерами довольно часто.

Иногда спамеры могут использовать в качестве веб-серверов сеть ботнет или прокси в качестве DNS-серверов, и так далее. Мы упоминали об этом раньше, но это еще один пример того, что чем больше у атакующего компьютеров, тем более он защищён, потому что может спрятать внутри них свою вредоносную начинку.

В лекционной статье говорится о партнёрских программах, о партнёрах-провайдерах.

Аффилированные провайдеры действуют как вредоносные клиринговые центры. Они помогают спамеру автоматизировать процессы взаимодействия с банками и платёжными системами.

Можно спросить, почему правоохранители не прикроют аффилированных провайдеров? Все дело в том, что эти провайдеры вроде как организация SPECTRE из фильмов про Джеймса Бонда. Сами по себе они очень децентрализованы, поэтому достаточно трудно указать на аффилированного провайдера на конкретном компьютере и просто отключить этот компьютер. Часто партнеры-провайдеры сами распространяют свои услуги, потому, например, для ФБР сложно просто прийти к такому провайдеру и потребовать у него отказа от подобной деятельности. В статье также отмечается, что законодательство в области защиты интеллектуальной собственности различается во многих странах, поэтому ФБР не может защитить права на нашу интеллектуальную собственность за пределами США.

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

52:00 мин


Полная версия курса доступна здесь.

Спасибо, что остаётесь с нами. Вам нравятся наши статьи? Хотите видеть больше интересных материалов? Поддержите нас оформив заказ или порекомендовав знакомым, 30% скидка для пользователей Хабра на уникальный аналог entry-level серверов, который был придуман нами для Вас: Вся правда о VPS (KVM) E5-2650 v4 (6 Cores) 10GB DDR4 240GB SSD 1Gbps от $20 или как правильно делить сервер? (доступны варианты с RAID1 и RAID10, до 24 ядер и до 40GB DDR4).

VPS (KVM) E5-2650 v4 (6 Cores) 10GB DDR4 240GB SSD 1Gbps до января бесплатно при оплате на срок от полугода, заказать можно тут.

Dell R730xd в 2 раза дешевле? Только у нас 2 х Intel Dodeca-Core Xeon E5-2650v4 128GB DDR4 6x480GB SSD 1Gbps 100 ТВ от $249 в Нидерландах и США! Читайте о том Как построить инфраструктуру корп. класса c применением серверов Dell R730xd Е5-2650 v4 стоимостью 9000 евро за копейки?

Let's block ads! (Why?)