Форум программистов, компьютерный форум, киберфорум
golander
Войти
Регистрация
Восстановить пароль
Блоги Сообщество Поиск Заказать работу  

Использование кэшей CPU: Максимальная производительность в Go

Запись от golander размещена 05.04.2025 в 16:55
Показов 3825 Комментарии 0

Нажмите на изображение для увеличения
Название: 3b7804e2-ba0c-4008-8371-9fee0e2b4d27.jpg
Просмотров: 149
Размер:	134.4 Кб
ID:	10535
Разработчикам хорошо известно, что эффективность кода зависит не только от алгоритмов и структур данных, но и от того, насколько удачно программа взаимодействует с железом. Среди множества факторов, влияющих на производительность, особое место занимает работа с кэш-памятью процессора. Именно кэш-память часто становится тем узким местом, которое ограничивает быстродействие даже самых элегантных алгоритмов.

Представьте: ваш код идеально структурирован, алгоритмическая сложность минимальна, но программа всё равно работает медленнее, чем хотелось бы. Причина может скрываться в неэффективном взаимодействии с кэш-памятью. Когда процессору требуется обработать данные, они должны сначала попасть в него из основной памяти. Этот путь не прямой — данные проходят через несколько уровней кэша, и каждый переход между уровнями требует определённого времени. Пример из практики: операция доступа к L1-кэшу занимает примерно 4 такта процессора, к L2-кэшу — около 12 тактов, а получение данных из оперативной памяти может потребовать более 100 тактов. Разница колоссальная! Поэтому грамотное использование кэш-памяти становится критически важным для высокопроизводительных систем.

История развития кэш-архитектур насчитывает уже несколько десятилетий. Первые кэши появились в мейнфреймах IBM в 1960-х годах как решение проблемы несоответствия скорости работы процессора и оперативной памяти. С тех пор архитектура кэшей непрерывно совершенствовалась - увеличивалось количество уровней, росли объёмы, улучшались алгоритмы предсказания и замещения данных. Современные процессоры обычно имеют трёхуровневую иерархию кэшей. L1-кэш наиболее быстрый, но имеет ограниченный объём (около 32-64 КБ на ядро), разделен на кэш инструкций и данных. L2-кэш больше (256-512 КБ на ядро) и чуть медленнее. L3-кэш самый объёмный (до нескольких МБ), он часто является общим для всех ядер процессора. Такая структура позволяет балансировать между скоростью доступа и объёмом хранимых данных.

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

Математически эффективность кэширования часто оценивают через показатель "hit ratio" — отношение количества успешных обращений к кэшу к общему числу обращений. Другие важные метрики включают среднее время доступа к памяти (AMAT, Average Memory Access Time), которое рассчитывается как:

AMAT = Hit Time + (Miss Rate × Miss Penalty)

где Hit Time — время успешного доступа к кэшу, Miss Rate — частота промахов, а Miss Penalty — штраф за промах, то есть время, необходимое для загрузки данных из следующего уровня памяти.

Исследования показывают, что разрыв между скоростью процессора и скоростью доступа к основной памяти продолжает увеличиваться. Если в 1980-х годах соотношение этих скоростей составляло примерно 1:1, то сегодня процессоры работают в сотни раз быстрее, чем осуществляется доступ к RAM. Этот феномен получил название "Memory Wall" (стена памяти) — фундаментальное ограничение, которое делает оптимизацию работы с кэшем не просто желательной, а жизненно необходимой.

В языке Go существует ряд особенностей, которые существенно влияют на эффективность взаимодействия с кэш-памятью. Например, размер и расположение структур данных, порядок инициализации полей, использование указателей вместо значений — всё это может критически влиять на производительность программы. При этом встроенный профайлер Go (pprof) не предоставляет прямой информации о кэш-промахах, что затрудняет диагностику проблем, связанных с кэшированием. Рассмотрим для примера простую, но распространенную ситуацию: обработку большого массива структур. Казалось бы, последовательный перебор элементов массива должен эффективно использовать кэш благодаря принципу пространственной локальности. Однако если размер структуры велик, а для обработки используется лишь малая часть её полей, процессор будет вынужден загружать в кэш большие объёмы "бесполезных" в данном контексте данных, что приведёт к вытеснению из кэша действительно нужной информации.

Современные методы оценки производительности кэш-систем выходят за рамки простой модели AMAT. Учитываются такие факторы, как ассоциативность кэша, стратегии вытеснения, предварительная выборка (prefetching), параллельные запросы и многое другое. Один из подходов — использование "Roofline model", которая позволяет визуализировать предельную производительность приложения в координатах "операционная интенсивность — производительность". Эта модель наглядно показывает, ограничена ли производительность программы пропускной способностью памяти или вычислительной мощностью процессора.

Для разработчиков на Go особенно важно понимать, как организована память в рантайме языка. Go использует механизм автоматического управления памятью со сборкой мусора, что удобно, но может создавать дополнительные сложности для оптимизации кэширования. Например, объекты в куче могут быть размещены непредсказуемым образом, что затрудняет обеспечение локальности данных. Интересный аспект — влияние сборщика мусора на эффективность использования кэш-памяти. С одной стороны, работа GC может вытеснять полезные данные из кэша. С другой стороны, современные алгоритмы сборки мусора (такие как используемый в Go concurrent mark-and-sweep с поколенческими оптимизациями) спроектированы с учётом минимизации воздействия на кэш-память. Ещё один фактор — интерфейсы в Go и их влияние на предсказуемость доступа к памяти. Использование интерфейсов вводит дополнительный уровень косвенности, поскольку обращение к методу объекта через интерфейс требует двойного разыменования указателя: сначала получение таблицы методов, затем получение адреса конкретного метода. Это может приводить к дополнительным кэш-промахам, особенно если код обрабатывает разнородные объекты через общий интерфейс.

В контексте многопоточного программирования, которое естественно для Go с его горутинами возникает дополнительная проблема — когерентность кэшей. Когда разные ядра процессора модифицируют данные, находящиеся в их локальных кэшах, необходим механизм обеспечения согласованности этих данных. Протоколы когерентности, такие как MESI (Modified, Exclusive, Shared, Invalid), решают эту задачу, но с определённым штрафом по производительности.

Архитектура кэш-памяти



Архитектура кэш-памяти современных процессоров — это сложная иерархическая система, которая эволюционировала на протяжении десятилетий. Рассмотрим типичную структуру кэшей в современном CPU. Кэш-память организована в виде нескольких уровней, каждый из которых характеризуется своей ёмкостью и временем доступа:

L1-кэш — самый быстрый и маленький. Обычно разделён на две части: кэш данных (L1d) и кэш инструкций (L1i). Его размер в современных процессорах составляет примерно 32-64 КБ на ядро. Доступ к L1-кэшу занимает всего 3-4 такта процессора.

L2-кэш — является промежуточным звеном. Его объём составляет обычно 256-512 КБ на ядро, а время доступа — около 10-12 тактов. L2-кэш обычно является унифицированным, то есть хранит как данные, так и инструкции.

L3-кэш — самый большой, но и самый медленный из кэшей. Его объём может достигать нескольких мегабайт (от 4 до 64 МБ в современных процессорах), а доступ требует 40-75 тактов. Особенность L3-кэша в том, что он, как правило, является общим для всех ядер процессора. В архитектуре Intel L3-кэш часто содержит копии данных из L1 и L2.

Физически кэш-память организована блоками, которые называются кэш-линиями. Размер кэш-линии в большинстве современных процессоров составляет 64 байта. Это означает, что даже если программе требуется всего один байт информации, в кэш будет загружен блок данных размером 64 байта. Это свойство крайне важно учитывать при проектировании структур данных и алгоритмов. Когда процессор обращается к памяти, сначала проверяется наличие нужных данных в L1-кэше. Если данные не найдены (происходит кэш-промах, cache miss), поиск продолжается в L2-кэше, затем в L3, и только потом — в оперативной памяти. Каждый промах приводит к значительной задержке.

Существуют различные политики замещения кэш-линий, которые определяют, какие данные будут вытеснены из кэша при необходимости загрузки новых. Наиболее распространённые стратегии:
  • LRU (Least Recently Used) — вытесняются данные, к которым дольше всего не обращались,
  • FIFO (First In, First Out) — вытесняются данные, которые были загружены раньше всего,
  • LFU (Least Frequently Used) — вытесняются наименее часто используемые данные,
  • Случайное замещение — выбирается случайная кэш-линия для замены.

Процессоры AMD и Intel имеют некоторые архитектурные различия в организации кэш-памяти. Например в процессорах AMD каждое ядро обычно имеет свой выделенный L2-кэш, а L3-кэш функционирует как "жертвенный кэш" (victim cache), хранящий данные, вытесненные из L1 и L2. В процессорах Intel L3-кэш обычно работает как инклюзивный кэш, содержащий копии всех данных из L1 и L2. Эти различия могут влиять на стратегию оптимизации для конкретной платформы.

Одна из серьёзных проблем в многопоточном программировании — false sharing (ложное разделение). Она возникает, когда разные потоки или горутины обращаются к разным переменным, которые по стечению обстоятельств расположены в одной кэш-линии. Это приводит к частой инвалидации кэша и снижению производительности. В языке Go эта проблема особенно актуальна при работе с параллельными вычислениями. Для иллюстрации можно привести пример: два ядра процессора одновременно обновляют разные счётчики, но если эти счётчики расположены в одной кэш-линии, то каждое обновление одного счётчика будет вызывать инвалидацию кэш-линии на другом ядре, что резко снизит производительность.

Для эффективного использования кэш-памяти необходимо учитывать её ассоциативность — способ организации соответствия между адресами оперативной памяти и ячейками кэша. Различают прямое отображение (direct mapped), полностью ассоциативное (fully associative) и наборно-ассоциативное (set associative) кэширование. Большинство современных процессоров используют наборно-ассоциативный кэш с различными степенями ассоциативности (обычно от 4 до 16 way).

В процессе эволюции вычислительных систем кэш-память становилась всё более сложной и многофункциональной. Особого внимания заслуживают механизмы когерентности кэшей, которые обеспечивают согласованность данных при параллельной обработке. Когерентность кэш-памяти — критический аспект в многоядерных системах. Когда несколько ядер одновременно работают с общими данными, необходимо гарантировать, что изменения, сделанные одним ядром, будут видны другим. Для этого используются специальные протоколы когерентности, наиболее распространённым из которых является MESI (Modified, Exclusive, Shared, Invalid):
  • Modified (M) — кэш-линия была изменена и отличается от данных в памяти.
  • Exclusive (E) — кэш-линия присутствует только в одном кэше.
  • Shared (S) — кэш-линия может быть в нескольких кэшах одновременно.
  • Invalid (I) — кэш-линия недействительна.

Работа этого протокола хорошо заметна в многопоточных Go-программах. Когда одна горутина модифицирует данные, кэш-линия переходит в состояние Modified. Если другая горутина, выполняющаяся на другом ядре, обращается к этим данным, происходит процесс инвалидации и обновления соответствующей кэш-линии. На практике это может приводить к существенным накладным расходам. Например, для простой операции атомарного инкремента счётчика требуется не только выполнить саму операцию, но и обеспечить согласованность кэшей всех ядер, что может занимать сотни тактов процессора. Поэтому в высоконагруженных Go-системах часто используют локальные счётчики для каждой горутины с периодической агрегацией результатов.

NUMA (Non-Uniform Memory Access) архитектуры добавляют дополнительный уровень сложности. В NUMA-системах процессоры имеют быстрый доступ к некоторой "локальной" памяти и более медленный — к "удалённой". Инвалидация кэша в таких системах особенно дорога, поскольку требует межпроцессорного взаимодействия. Для Go-программ, запущенных на NUMA-системах, стратегия размещения данных становится критически важной — данные, часто используемые одной горутиной, должны находиться в локальной для соответствующего ядра памяти.

Взаимодействие кэш-линий с векторными инструкциями (SIMD — Single Instruction, Multiple Data) — ещё один важный аспект. Современные процессоры поддерживают векторные расширения (SSE, AVX, NEON), которые позволяют обрабатывать несколько элементов данных одной инструкцией. Эффективность этих инструкций сильно зависит от выравнивания данных и их размещения в кэш-линиях. Go поддерживает использование SIMD-инструкций через встроенные функции компилятора и ассемблерные вставки. Однако важно понимать, что даже если код использует векторные инструкции, неоптимальное размещение данных в памяти может свести на нет все преимущества. Поэтому для высокопроизводительных вычислений критично обеспечить правильное выравнивание структур данных.

Предварительная загрузка данных (prefetching) — механизм, который позволяет уменьшить количество кэш-промахов за счёт предугадывания, какие данные понадобятся процессору в ближайшем будущем. Существуют как аппаратные предсказатели, встроенные в процессор, так и программные механизмы, активируемые специальными инструкциями. Аппаратный prefetching работает лучше всего при последовательном доступе к памяти, что ещё раз подчёркивает важность продуманной организации данных. Программный prefetching можно использовать в Go через ассемблерные вставки, однако на практике его применение требует тщательного профилирования и тестирования, поскольку неправильное использование может даже ухудшить производительность.

Понимание принципов работы кэш-памяти особенно важно при проектировании структур данных в Go. Например, рассмотрим стандартную структуру slice в Go. Слайс представляет собой структуру из трёх полей: указатель на базовый массив, длина и ёмкость. Каждое обращение к элементу слайса требует разыменования указателя, что потенциально может привести к кэш-промаху. Однако благодаря последовательному расположению элементов в базовом массиве, последующие обращения с большой вероятностью будут попадать в уже загруженную кэш-линию.

Архитектурные особенности современных процессоров также включают специализированные кэши для определённых типов данных. Например, TLB (Translation Lookaside Buffer) — кэш для таблиц трансляции виртуальных адресов, или кэш трассировки ветвлений (Branch Target Buffer, BTB), который используется для предсказания переходов. Эффективность этих специализированных кэшей также влияет на общую производительность программ на Go. При работе с большими объёмами данных особое значение приобретает так называемое выравнивание данных (data alignment). Процессоры обычно оптимизированы для работы с данными, которые расположены в памяти по адресам, кратным размеру этих данных (например, 4-байтовые целые числа должны начинаться с адресов, кратных 4). Неправильное выравнивание может привести не только к снижению производительности, но и, в некоторых архитектурах, к ошибкам доступа к памяти. В Go выравнивание структур происходит автоматически, но разработчик может влиять на этот процесс, изменяя порядок полей в структуре. Правильное расположение полей может значительно улучшить локальность данных и, как следствие, эффективность использования кэш-памяти.

CPU греется до 75 градусов , проц : Intel Pentium(R) 4 CPU 3.00GHz
У меня проблема, CPU греется до 75 градусов , проц : Intel Pentium(R) 4 CPU 3.00GHz материнка :...

Выключается ПК "Warning! CPU has been changed. Please re-enter CPU settings in the CMOS"
пишет Warning! CPU has been changed. Please re-enter CPU settings in the CMOS setup and remember to...

CPU-Z показывает частоту CPU в 2 раза меньше, чем должно быть
Ребят, хелп! Недавно проапгрейдил ПК, заменил материнку на ASRock b450m steel legend, процессор на...

CPU VCore и CPU VID в Ryzen 5 3600
Связка: MSI B450 Gaming Plus Max + Ryzen 5 3600 биос последний.АвтоБуст включен.Выставлен...


Оптимизация доступа к данным



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

Пространственная локальность означает, что после обращения к определённому адресу памяти высока вероятность обращения к соседним адресам. Именно на этом принципе основана работа кэш-линий — когда процессор запрашивает один байт данных, в кэш загружается целый блок (64 байта в современных архитектурах). Временная локальность предполагает, что если к данным обратились сейчас, велика вероятность, что к ним обратятся снова в ближайшем будущем. Рассмотрим пример из реального проекта на Go, где неоптимальная структура данных приводила к серьёзным проблемам с производительностью:

Go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
type User struct {
    Login   string
    Active  bool
    Icon    [128 * 128]byte // 16КБ данных
    Country string
}
 
// Функция подсчёта активных пользователей по странам
func CountryCount(users []User) map[string]int {
    counts := make(map[string]int)
    for _, u := range users {
        if !u.Active {
            continue
        }
        counts[u.Country]++
    }
    return counts
}
В этом коде каждый объект User содержит поле Icon размером 16 КБ. При обработке массива таких структур для подсчёта количества активных пользователей по странам нам нужны только поля Active и Country, но процессор вынужден загружать в кэш всю структуру целиком, включая большое поле Icon, которое занимает несколько кэш-линий. Это приводит к постоянным кэш-промахам и значительному снижению производительности. Оптимизированный вариант может выглядеть так:

Go
1
2
3
4
5
6
7
8
type Image []byte
 
type User struct {
    Login   string
    Active  bool
    Icon    Image    // Слайс вместо массива
    Country string
}
Такое изменение радикально уменьшает размер структуры User, поскольку теперь поле Icon занимает всего 24 байта (8 байт для указателя на данные, 8 байт для длины и 8 байт для ёмкости). Весь объект теперь помещается в одну кэш-линию, что значительно улучшает локальность данных. Тесты производительности показывают впечатляющие результаты: оптимизированная версия работает примерно в 40 раз быстрее исходной. Количество кэш-промахов сокращается с миллиардов до миллионов, что подтверждает эффективность подхода.

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

Ещё один эффективный приём — структурирование данных по принципу SoA (Structure of Arrays) вместо AoS (Array of Structures). Вместо массива структур создаётся структура массивов, что позволяет загружать в кэш только те поля, которые действительно нужны для текущей операции:

Go
1
2
3
4
5
6
7
8
9
10
11
12
// AoS: Array of Structures
type UserAoS struct {
    Users []User
}
 
// SoA: Structure of Arrays
type UserSoA struct {
    Logins   []string
    Active   []bool
    Icons    []Image
    Countries []string
}
При обработке такой структуры процессор может загружать в кэш только нужные массивы (например, Active и Countries для подсчёта статистики), что значительно повышает эффективность кэширования.
Выравнивание структур данных также играет критическую роль. Компилятор Go автоматически добавляет отступы (padding) между полями структуры для их выравнивания, но разработчик может влиять на этот процесс, изменяя порядок полей. Правило простое: размещайте поля в порядке уменьшения их размера, начиная с самых больших:

Go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// Неоптимальное выравнивание
type Config struct {
    Flag  bool       // 1 байт + 7 байт padding
    Count int64      // 8 байт
    Name  string     // 16 байт (указатель + длина)
}
// Общий размер: 32 байта
 
// Оптимальное выравнивание
type Config struct {
    Name  string     // 16 байт
    Count int64      // 8 байт
    Flag  bool       // 1 байт + 7 байт padding
}
// Общий размер: 32 байта
Хотя общий размер структуры не изменился, в оптимизированном варианте минимизировано количество отступов, что может быть особенно важно для больших коллекций таких структур.

Фрагментация данных — ещё одна проблема, которая может негативно влиять на эффективность кэширования. Когда данные разбросаны по памяти, процессор не может эффективно использовать принцип пространственной локальности. В Go эта проблема может возникать при работе со слайсами, которые были частично перераспределены, или с картами (maps), которые хранят данные в хэш-таблицах с потенциально несмежными блоками памяти.

Для предотвращения фрагментации полезно использовать пулы объектов или предварительное выделение памяти с указанием достаточной ёмкости:

Go
1
2
3
4
5
6
7
8
9
10
11
// Вместо постепенного добавления элементов
data := make([]int, 0)
for i := 0; i < n; i++ {
    data = append(data, i) // Может вызывать перераспределение памяти
}
 
// Лучше предварительно выделить память
data := make([]int, 0, n) // Указываем емкость
for i := 0; i < n; i++ {
    data = append(data, i) // Не требует перераспределения
}
Техника компактного представления данных тоже может значительно повысить эффективность кэширования. Например, для хранения множества логических значений вместо массива bool (где каждое значение занимает как минимум 1 байт) можно использовать битовые операции над целыми числами, где каждый бит представляет одно логическое значение.

В Go также существуют механизмы memory pooling, которые помогают снизить нагрузку на сборщик мусора и улучшить локальность данных. Пакет sync.Pool предоставляет возможность повторного использования временных объектов, предотвращая излишние выделения памяти:

Go
1
2
3
4
5
6
7
8
9
10
11
12
var bufferPool = sync.Pool{
    New: func() interface{} {
        return make([]byte, 4096)
    },
}
 
func ProcessData() {
    buffer := bufferPool.Get().([]byte)
    defer bufferPool.Put(buffer)
    
    // Использование буфера...
}
Такой подход не только снижает нагрузку на сборщик мусора, но и способствует лучшей локальности данных, так как объекты, часто используемые вместе, с большей вероятностью будут располагаться в соседних областях памяти.

Механизмы предварительной загрузки данных (prefetching) используются для уменьшения латентности доступа к памяти. Хотя Go напрямую не предоставляет API для управления prefetching, компилятор и рантайм языка часто могут автоматически оптимизировать доступ к памяти. Для случаев, когда требуется ручная оптимизация, можно использовать ассемблерные вставки:

Go
1
2
3
4
5
6
7
8
9
10
11
//go:noescape
func prefetch(addr unsafe.Pointer) 
 
func ProcessLargeArray(data []int) {
    for i := 0; i < len(data); i++ {
        if i+64 < len(data) {
            prefetch(unsafe.Pointer(&data[i+64]))
        }
        // Обработка текущего элемента...
    }
}
Такой код подсказывает процессору заранее загрузить в кэш данные, которые понадобятся через 64 итерации цикла. Однако использование подобных оптимизаций требует тщательного тестирования и профилирования, поскольку неправильный prefetching может даже ухудшить производительность.

Выбор подходящих структур данных критически важен для эффективного использования кэша. Например, при работе с большими массивами данных вместо обычных слайсов можно использовать специализированные структуры, такие как B-деревья или Skip-листы, которые обеспечивают лучшую локальность при определённых паттернах доступа. Интересный аспект — влияние интерфейсов Go на предсказуемость кэширования. Интерфейсы в Go реализуются через таблицы виртуальных методов, что вводит дополнительный уровень косвенности при вызове методов. Это может приводить к дополнительным кэш-промахам, особенно если код активно использует полиморфизм:

Go
1
2
3
4
5
6
7
8
9
10
11
type Processor interface {
    Process(data []byte) error
}
 
func ProcessBatch(processors []Processor, batch [][]byte) {
    for _, proc := range processors {
        for _, data := range batch {
            proc.Process(data)  // Потенциальный кэш-промах при разрешении интерфейса
        }
    }
}
В таких случаях может быть эффективнее использовать статический полиморфизм через генерики (доступны с Go 1.18) или даже просто дублировать код для разных типов, если производительность критична:

Go
1
2
3
4
5
6
7
8
// Версия с использованием генериков
func ProcessBatchGeneric[P interface{ Process([]byte) error }](processors []P, batch [][]byte) {
    for _, proc := range processors {
        for _, data := range batch {
            proc.Process(data)  // Компилятор может оптимизировать вызов
        }
    }
}
При проектировании параллельных алгоритмов в Go особое внимание следует уделять разделению данных между горутинами. Часто эффективнее организовать обработку таким образом, чтобы каждая горутина работала со своей частью данных, минимизируя общие ресурсы. Это не только снижает необходимость в синхронизации, но и улучшает локальность кэша для каждого ядра процессора.
Рассмотрим пример параллельной агрегации данных:

Go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
func ParallelSum(data []int) int {
    cpus := runtime.NumCPU()
    var wg sync.WaitGroup
    results := make([]int, cpus)
    
    chunkSize := (len(data) + cpus - 1) / cpus
    for i := 0; i < cpus; i++ {
        wg.Add(1)
        go func(workerID int) {
            defer wg.Done()
            
            start := workerID * chunkSize
            end := start + chunkSize
            if end > len(data) {
                end = len(data)
            }
            
            localSum := 0
            for j := start; j < end; j++ {
                localSum += data[j]
            }
            
            results[workerID] = localSum
        }(i)
    }
    
    wg.Wait()
    
    totalSum := 0
    for _, sum := range results {
        totalSum += sum
    }
    
    return totalSum
}
В этом коде каждая горутина обрабатывает свой непрерывный участок массива, что максимизирует локальность данных и эффективность кэширования. Только после завершения параллельной обработки результаты объединяются, что требует минимальной синхронизации.

Еще одна техника оптимизации — использование структур с выровненными полями для атомарных операций. В Go атомарные операции требуют выравнивания данных, при этом невыровненный доступ может привести к существенному падению производительности:

Go
1
2
3
4
5
6
7
8
type Counter struct {
    value uint64  // Должен быть выровнен на 8-байтовую границу для эффективных атомарных операций
    _     [56]byte  // Padding для предотвращения false sharing
}
 
func (c *Counter) Increment() {
    atomic.AddUint64(&c.value, 1)
}
Добавление padding (56 байт в примере) гарантирует, что два соседних счётчика не попадут в одну кэш-линию, что устраняет проблему false sharing при параллельном инкрементировании разных счётчиков.

При работе с большими объемами данных (например, при обработке изображений или научных вычислениях) важно учитывать порядок обхода многомерных массивов. В Go многомерные массивы хранятся в памяти в порядке "строка за строкой", поэтому для оптимального использования кэша предпочтительнее обход в том же порядке:

Go
1
2
3
4
5
6
7
8
9
10
11
12
13
// Неэффективный обход: кэш-промахи на каждой итерации по столбцам
for col := 0; col < width; col++ {
    for row := 0; row < height; row++ {
        process(image[row][col])
    }
}
 
// Эффективный обход: использует пространственную локальность
for row := 0; row < height; row++ {
    for col := 0; col < width; col++ {
        process(image[row][col])
    }
}
Важно отметить, что оптимизация для кэш-памяти не всегда должна быть приоритетом. Прежде чем приступать к низкоуровневым оптимизациям, необходимо измерить производительность и определить реальные узкие места. Часто алгоритмические изменения дают гораздо больший эффект, чем микрооптимизации.

Практические приемы



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

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

Bash
1
2
# Использование утилиты perf на Linux
perf stat -e cache-misses ./my_program
Эта команда покажет количество кэш-промахов, произошедших во время выполнения программы. Для более детального анализа можно использовать расширенные функции:

Bash
1
2
3
# Детальное профилирование кэш-событий
perf record -e cache-misses:u -c 1000 ./my_program
perf report
В Go стандартный профилировщик pprof не отслеживает кэш-промахи напрямую, но косвенную информацию можно получить из профилей CPU и аллокаций:

Go
1
2
3
4
5
6
import "runtime/pprof"
 
f, _ := os.Create("cpu.prof")
pprof.StartCPUProfile(f)
// Код программы
pprof.StopCPUProfile()
Бенчмарки — неотъемлемая часть процесса оптимизации. Они позволяют измерять производительность в контролируемых условиях и сравнивать эффективность различных подходов:

Go
1
2
3
4
5
6
7
8
9
10
11
func BenchmarkProcessing(b *testing.B) {
    data := prepareTestData()
    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        result := processingFunction(data)
        // Предотвращаем оптимизацию неиспользуемого результата
        if result == nil {
            b.Fatal("Unexpected nil result")
        }
    }
}
При запуске бенчмарков полезно использовать флаг -benchmem, который показывает информацию о выделениях памяти — ключевой фактор, влияющий на эффективность кэширования.
После выявления проблемных мест переходим к конкретным оптимизациям. Одна из эффективных техник — преварительное выделение памяти со сжатыми структурами данных:

Go
1
2
3
4
5
6
7
8
9
10
11
// До оптимизации
data := make([]int, 0)
for i := 0; i < size; i++ {
    data = append(data, getValue(i))
}
 
// После оптимизации
data := make([]int, size)
for i := 0; i < size; i++ {
    data[i] = getValue(i)
}
Эта простая модификация устраняет множественные перераспределения памяти, улучшая пространственную локальность данных и уменьшая фрагментацию.
Для структур данных, которые не умещаются в кэш целиком, эффективным решением может быть их разделение на части (шардирование):

Go
1
2
3
4
5
6
7
8
9
type ShardedCounter struct {
    counters [256]uint64
}
 
func (sc *ShardedCounter) Increment(key uint64) {
    // Используем младшие 8 бит ключа для выбора шарда
    shard := key & 0xFF
    atomic.AddUint64(&sc.counters[shard], 1)
}
Такая структура минимизирует конфликты между потоками, удерживая разные шарды в отдельных кэш-линиях.
При работе с большими коллекциями объектов плоские структуры часто эффективнее иерархических:

Go
1
2
3
4
5
6
7
8
9
10
11
12
13
// Иерархическая структура (менее эффективная для кэша)
type Node struct {
    Value int
    Left  *Node
    Right *Node
}
 
// Плоская структура (более кэш-дружественная)
type FlatTree struct {
    Values []int
    Lefts  []int  // индексы левых потомков
    Rights []int  // индексы правых потомков
}
Плоская структура размещает все данные в непрерывных массивах, что улучшает предсказуемость доступа и эффективность предварительной загрузки. Для операций, требующих частого обращения к памяти, полезной техникой является разворачивание циклов (loop unrolling):

Go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// До оптимизации
sum := 0
for i := 0; i < len(data); i++ {
    sum += data[i]
}
 
// После оптимизации
sum := 0
remainder := len(data) % 4
for i := 0; i < remainder; i++ {
    sum += data[i]
}
for i := remainder; i < len(data); i += 4 {
    sum += data[i] + data[i+1] + data[i+2] + data[i+3]
}
Такая модификация уменьшает количество проверок условий и ветвлений, потенциально позволяя процессору лучше предсказывать доступ к памяти.
В микросервисной архитектуре, где каждый сервис может выполняться на отдельной физической машине локальное кэширование часто используемых данных критически важно. Например, можно кэшировать результаты удалённых вызовов:

Go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
type Cache struct {
    mu    sync.RWMutex
    items map[string]cachedItem
}
 
type cachedItem struct {
    value      interface{}
    expiration time.Time
}
 
func (c *Cache) Get(key string, fetch func() (interface{}, error)) (interface{}, error) {
    c.mu.RLock()
    item, found := c.items[key]
    c.mu.RUnlock()
    
    if found && time.Now().Before(item.expiration) {
        return item.value, nil
    }
    
    // Ключа нет в кэше или он устарел - получаем новое значение
    value, err := fetch()
    if err != nil {
        return nil, err
    }
    
    // Сохраняем в кэше
    c.mu.Lock()
    c.items[key] = cachedItem{
        value:      value,
        expiration: time.Now().Add(c.ttl),
    }
    c.mu.Unlock()
    
    return value, nil
}
Локальное кэширование актуально не только для микросервисов, но и для высоконагруженных монолитных приложений. Например, при частых обращениях к базе данных можно реализовать двухуровневый кэш:

Go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
type DatabaseCache struct {
    l1     map[string]interface{} // Часто используемые данные в памяти
    l2     *lru.Cache            // Большой LRU-кэш для менее частых запросов
    db     Database              // Интерфейс к реальной базе данных
    mu     sync.RWMutex
}
 
func (c *DatabaseCache) Get(key string) (interface{}, error) {
    // Сначала проверяем L1-кэш для сверхбыстрого доступа
    c.mu.RLock()
    if val, ok := c.l1[key]; ok {
        c.mu.RUnlock()
        return val, nil
    }
    c.mu.RUnlock()
    
    // Проверяем L2-кэш
    if val, ok := c.l2.Get(key); ok {
        return val, nil
    }
    
    // Обращаемся к базе данных
    val, err := c.db.Query(key)
    if err != nil {
        return nil, err
    }
    
    // Обновляем L2-кэш
    c.l2.Add(key, val)
    return val, nil
}
Взаимодействие сборщика мусора Go с кэш-памятью — важный аспект, который часто упускают из виду. Сборщик мусора может вытеснять полезные данные из кэша во время своей работы. Для минимизации этого эффекта полезны следующие приемы:

1. Группировка аллокаций — старайтесь выделять память большими блоками, а не множеством маленьких:

Go
1
2
3
4
5
6
7
8
9
10
11
12
// Вместо этого
items := make([]*Item, size)
for i := 0; i < size; i++ {
    items[i] = &Item{}  // Отдельная аллокация для каждого элемента
}
 
// Делайте так
itemsBlock := make([]Item, size)  // Одна аллокация для всех
items := make([]*Item, size)
for i := 0; i < size; i++ {
    items[i] = &itemsBlock[i]  // Указатели на существующие элементы
}
2. Контроль частоты сборки мусора через настройку GOGC или использование runtime.GC() в стратегически выбранных точках программы:

Go
1
2
3
4
5
// Перед критически важной операцией
runtime.GC()  // Форсируем сборку мусора
setGCPercent := debug.SetGCPercent(-1)  // Временно отключаем GC
// Выполняем операцию, чувствительную к задержкам
debug.SetGCPercent(setGCPercent)  // Восстанавливаем настройки GC
Для ресурсоемких структур данных, таких как графы или большие деревья, эффективным подходом может быть собственная реализация пулов объектов:

Go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
type NodePool struct {
    pool sync.Pool
}
 
func NewNodePool() *NodePool {
    return &NodePool{
        pool: sync.Pool{
            New: func() interface{} {
                return &Node{}
            },
        },
    }
}
 
func (p *NodePool) Get() *Node {
    return p.pool.Get().(*Node)
}
 
func (p *NodePool) Put(node *Node) {
    // Сброс состояния перед возвратом в пул
    *node = Node{}
    p.pool.Put(node)
}
Кэш-осознанные алгоритмы сортировки и поиска могут существенно ускорить операции с большими объемами данных. Например, при работе с огромными массивами, кэш-эффектвная реализация быстрой сортировки может учитывать размер L1-кэша:

Go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
func CacheAwareQuickSort(data []int) {
    const l1CacheThreshold = 16 * 1024 / 8  // Примерный размер L1 в int64
    quickSortRecursive(data, 0, len(data)-1, l1CacheThreshold)
}
 
func quickSortRecursive(data []int, low, high, threshold int) {
    if high-low <= 0 {
        return
    }
    
    // Для маленьких подмассивов, умещающихся в кэш, используем простую сортировку вставками
    if high-low < threshold {
        insertionSort(data[low:high+1])
        return
    }
    
    // Обычный алгоритм быстрой сортировки для больших массивов
    pivot := partition(data, low, high)
    quickSortRecursive(data, low, pivot-1, threshold)
    quickSortRecursive(data, pivot+1, high, threshold)
}
При реализации структур данных для высоконагруженных систем стоит рассмотреть специализированные кэш-осознанные альтернативы. Например, вместо стандартных бинарных деревьев поиска эффективнее использовать B-деревья или критбиты, которые лучше соответствуют архитектуре кэша:

Go
1
2
3
4
5
6
7
// B-дерево: узел содержит несколько ключей и указателей на потомков
type BTreeNode struct {
    keys     [2*t-1]int  // t - минимальная степень дерева
    children [2*t]*BTreeNode
    n        int         // количество ключей
    leaf     bool
}
При работе в контейнерной среде и Kubernetes-кластерах нужно учитывать, что ресурсы процессора могут быть ограничены. В таких случаях полезно настраивать параметры Go runtime в соответствии с выделенными ресурсами:

Go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func init() {
    // Настраиваем количество процессоров и GC в соответствии с ограничениями контейнера
    if cpuLimit := os.Getenv("CPU_LIMIT"); cpuLimit != "" {
        if limit, err := strconv.Atoi(cpuLimit); err == nil {
            runtime.GOMAXPROCS(limit)
        }
    }
    
    // Агрессивнее собираем мусор в условиях ограниченной памяти
    if memLimit := os.Getenv("MEMORY_LIMIT_MB"); memLimit != "" {
        if limit, err := strconv.Atoi(memLimit); err == nil && limit < 1024 {
            debug.SetGCPercent(20)  // Более частая сборка мусора
        }
    }
}
Интересный прием — использование префетчинга данных через "ходячего впереди" (scout thread). Отдельная горутина может загружать данные в кэш перед тем, как они понадобятся основному коду:

Go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
func ProcessLargeArray(data []int) {
    done := make(chan struct{})
    prefetchDistance := 1024  // Расстояние опережения
    
    // Запускаем "разведчика"
    go func() {
        for i := 0; i < len(data); i += 64 {  // Шаг в размер кэш-линии
            if i+prefetchDistance < len(data) {
                _ = data[i+prefetchDistance]  // Просто прикасаемся к данным, чтобы они попали в кэш
            }
            
            select {
            case <-done:
                return
            default:
                // Продолжаем
            }
        }
    }()
    
    // Основной код обработки
    for i := 0; i < len(data); i++ {
        // Обработка data[i]
    }
    
    close(done)  // Сигнализируем разведчику о завершении
}

Современные исследования и метрики



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

Исследования показывают, что в типичных приложениях до 70% времени выполнения может быть потрачено на ожидание данных из памяти. При этом правильная организация кода способна снизить эту цифру до 20-30%, что дает колоссальный прирост производительности. Особенно заметны эти эффекты в вычислительно-интенсивных задачах, характерных для обработки больших данных и научных вычислений. Современные экспериментальные данные демонстрируют, что даже небольшие изменения в расположении данных в памяти могут приводить к значительным изменениям производительности. Например, исследования структур данных для графов показали, что кэш-осознанные реализации превосходят традиционные варианты в 2-5 раз на типичных задачах обхода и поиска.

Для количественной оценки эффективности кэширования используются различные метрики:

1. MPKI (Misses Per Kilo Instructions) – количество кэш-промахов на тысячу инструкций. Значения менее 1 считаются хорошими, более 10 указывают на серьезные проблемы.
2. CPI (Cycles Per Instruction) – количество тактов на инструкцию. Этот показатель косвенно характеризует эффективность использования кэша, поскольку кэш-промахи существенно увеличивают CPI.
3. Bandwidth Utilization – степень использования пропускной способности памяти. Высокие значения (более 80%) могут указывать на проблемы с локальностью данных.
4. LLC (Last Level Cache) Hit Rate – процент успешных обращений к последнему уровню кэша. Для типичных приложений хорошими считаются значения выше 90%.

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

Bash
1
2
3
4
5
# Профилирование с VTune Profiler от Intel
vtune -collect memory-access ./my_program
 
# Использование AMD μProf
uprofile --event=cache-misses ./my_program
Эти инструменты позволяют получать детальную информацию не только о кэш-промахах, но и о других аспектах взаимодействия программы с памятью: предвыборке, когерентности кэшей, частоте доступа к разным областям памяти.
Одно из интересных направлений – визуализация доступа к памяти с помощью тепловых карт. Эта техника позволяет наглядно увидеть, какие участки памяти используются наиболее интенсивно, и выявить неоптимальные паттерны доступа:

Go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// Пример кода для сбора данных о доступе к памяти
type MemoryAccess struct {
    Address uintptr
    Count   int
}
 
var accessMap = make(map[uintptr]int)
var mapMutex sync.Mutex
 
func recordAccess(addr uintptr) {
    mapMutex.Lock()
    accessMap[addr]++
    mapMutex.Unlock()
}
 
// После сбора данных они визуализируются
// с помощью специальных инструментов
Результаты таких визуализаций часто выявляют неочевидные проблемы, например, случайный доступ к памяти в код, который должен работать последовательно, или неожиданную фрагментацию данных.

Сравнительный анализ различных стратегий кэширования показывает, что универсальных решений не существует – оптимальный подход сильно зависит от характеристик конкретной задачи. Например, для операций чтения-модификации-записи (read-modify-write) обычно эффективнее использовать write-back кэширование, в то время как для потоковой обработки данных только для чтения лучше подходят методы предварительной загрузки.

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

Assembler
1
2
3
4
5
// Пример ассемблерной вставки для предзагрузки данных (x86-64)
TEXT ·prefetchNTA(SB),NOSPLIT,$0-8
    MOVQ ptr+0(FP), AX
    PREFETCHNTA (AX)
    RET
Значительный интерес представляют исследования в области кэш-обфускации и защиты от атак по сторонним каналам (side-channel attacks). Эти атаки используют информацию о кэш-промахах для извлечения конфиденциальных данных, таких как криптографические ключи. Для защиты от них разрабатываются специальные техники, которые делают паттерны доступа к памяти непредсказуемыми для атакующего. В Go-приложениях, обрабатывающих чувствительные данные, эти соображения становятся всё более актуальными.

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

Go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
type CacheAwareAlgorithm struct {
    blockSize      int
    prefetchDistance int
    processingOrder []int
}
 
func NewAdaptiveAlgorithm() *CacheAwareAlgorithm {
    // Определяем характеристики кэша текущего CPU
    l1Size := detectL1CacheSize()
    l1LineSize := detectL1LineSize()
    
    return &CacheAwareAlgorithm{
        blockSize:      l1Size / 4, // Эвристика: блок составляет 1/4 размера L1
        prefetchDistance: l1Size / l1LineSize, // Дистанция предзагрузки
        processingOrder: determineOptimalOrder(),
    }
}
Такие самонастраивающиеся системы показывают выигрыш до 30% по сравнению с традиционными статическими алгоритмами, особенно в гетерогенных вычислительных средах, где характеристики оборудования могут варьироваться.

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

Go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
type MLPrefetcher struct {
    model      *ml.Model
    historyBuffer []memAccess
    predictions   chan uintptr
}
 
func (p *MLPrefetcher) RecordAccess(addr uintptr) {
    p.historyBuffer = append(p.historyBuffer, memAccess{addr: addr, time: time.Now()})
    if len(p.historyBuffer) > bufferSize {
        p.historyBuffer = p.historyBuffer[1:]
    }
    
    go p.generatePredictions()
}
 
func (p *MLPrefetcher) generatePredictions() {
    // Получаем предсказание следующих адресов на основе исторических данных
    nextAddrs := p.model.Predict(p.historyBuffer)
    
    // Отправляем предсказания для предзагрузки
    for _, addr := range nextAddrs {
        p.predictions <- addr
    }
}
Предиктивная аналитика использования кэша позволяет выявлять потенциальные проблемы производительности ещё до их возникновения. Современные системы мониторинга анализируют тенденции в использовании кэша и предупреждают о возможных узких местах, давая разработчикам время для проактивной оптимизации.

В области архитектуры процессоров исследуются новые технологии, такие как неоднородные кэши (heterogeneous caches), где различные участки кэш-памяти оптимизированы для разных типов данных. Эта концепция напоминает принцип работы современных систем хранения данных с разделением на "горячие" и "холодные" данные, но на уровне процессорного кэша. Виртуализация кэш-памяти также становится предметом активных исследований. В условиях, когда на одном физическом сервере выполняется множество виртуальных машин или контейнеров, возникает проблема справедливого распределения ресурсов кэша между конкурирующими процессами. Новые алгоритмы позволяют изолировать и гарантировать определённую долю кэша для критически важных приложений.

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

процесор AMD Dual Core 6000+ его максимальная температура?
процесор AMD Dual Core 6000+ его максимальная температура? блин греется на играх аж стенка греется...

Установка Compro E 800 на Windows 7(максимальная)
Помогите пожалуйста с Compro E 800 на Windows 7(максимальная).не получается сохранить настройки...

Максимальная длина USB 2.0. Что будет при достижении длины в 5 м?
Слышал, что &quot;максимальной длиной&quot; для USB 2.0 является 5 метров. 2 вопроса: 1. Что значит...

Максимальная полоса пропускания памяти
Доброго времени суток! Из чего складывается максимальная полоса пропускания памяти? В железе не...

i7-4770K и максимальная частота памяти
Привет! Собираю такую систему: процик Intel Core i7-4770K Haswell (3500MHz, LGA1150, L3 8192Kb),...

FX-9370: Какая максимальная температура для моего процессора?
имеется AMD FX-9370 4.4 GHz@5GHz при кодировании видео нечего не зависает, нечего не глючит только...

Максимальная частота процессора Intel
Здравствуйте, может уже кто-то сталкивался с этим процессором и аналогичным вопросом. Перейду сразу...

Не максимальная частота у ЦП
Впервые сталкиваюсь с подобным. Как бороться?

Какая максимальная поддерживаемая частота процессора для материнки ASUS P5K-V ?
Материнк ASUS P5K-V максимум процессор поддерживает ГЦ? Скажите плиз на сколько максимум можно...

Какая максимальная частота для процессора Athlon X4 635?
Какая максимальная частота для процессора Athlon X4 635?Мне удалось разогнать до 3,4 ГГЦ с...

Максимальная допустимая, Безопасная, температура комплектующих
Доброго времени суток ! Сколько же инфы по поводу температуры процессоров, чипсэтов материнских...

Максимальная поддержеваемая частота оперативной памяти AMD Phenom II X4 965 BE
Собственно вопрос в том может ли этот процессор работать с памятью с частотой 1600 МГц ? Я как...

Размещено в Без категории
Надоела реклама? Зарегистрируйтесь и она исчезнет полностью.
Всего комментариев 0
Комментарии
 
Новые блоги и статьи
Музыка, написанная Искусственным Интеллектом
volvo 04.12.2025
Всем привет. Некоторое время назад меня заинтересовало, что уже умеет ИИ в плане написания музыки для песен, и, собственно, исполнения этих самых песен. Стихов у нас много, уже вышли 4 книги, еще 3. . .
От async/await к виртуальным потокам в Python
IndentationError 23.11.2025
Армин Ронахер поставил под сомнение async/ await. Создатель Flask заявляет: цветные функции - провал, виртуальные потоки - решение. Не threading-динозавры, а новое поколение лёгких потоков. Откат?. . .
Поиск "дружественных имён" СОМ портов
Argus19 22.11.2025
Поиск "дружественных имён" СОМ портов На странице: https:/ / norseev. ru/ 2018/ 01/ 04/ comportlist_windows/ нашёл схожую тему. Там приведён код на С++, который показывает только имена СОМ портов, типа,. . .
Сколько Государство потратило денег на меня, обеспечивая инсулином.
Programma_Boinc 20.11.2025
Сколько Государство потратило денег на меня, обеспечивая инсулином. Вот решила сделать интересный приблизительный подсчет, сколько государство потратило на меня денег на покупку инсулинов. . . .
Ломающие изменения в C#.NStar Alpha
Etyuhibosecyu 20.11.2025
Уже можно не только тестировать, но и пользоваться C#. NStar - писать оконные приложения, содержащие надписи, кнопки, текстовые поля и даже изображения, например, моя игра "Три в ряд" написана на этом. . .
Мысли в слух
kumehtar 18.11.2025
Кстати, совсем недавно имел разговор на тему медитаций с людьми. И обнаружил, что они вообще не понимают что такое медитация и зачем она нужна. Самые базовые вещи. Для них это - когда просто люди. . .
Создание Single Page Application на фреймах
krapotkin 16.11.2025
Статья исключительно для начинающих. Подходы оригинальностью не блещут. В век Веб все очень привыкли к дизайну Single-Page-Application . Быстренько разберем подход "на фреймах". Мы делаем одну. . .
Фото: Daniel Greenwood
kumehtar 13.11.2025
Расскажи мне о Мире, бродяга
kumehtar 12.11.2025
— Расскажи мне о Мире, бродяга, Ты же видел моря и метели. Как сменялись короны и стяги, Как эпохи стрелою летели. - Этот мир — это крылья и горы, Снег и пламя, любовь и тревоги, И бескрайние. . .
PowerShell Snippets
iNNOKENTIY21 11.11.2025
Модуль PowerShell 5. 1+ : Snippets. psm1 У меня модуль расположен в пользовательской папке модулей, по умолчанию: \Documents\WindowsPowerShell\Modules\Snippets\ А в самом низу файла-профиля. . .
КиберФорум - форум программистов, компьютерный форум, программирование
Powered by vBulletin
Copyright ©2000 - 2025, CyberForum.ru