В статье описана работа с битовыми полями на языке C# (Working with Bitfields in C#).
[Как работать с битовыми полями на C#]
Битовые поля хороши для обработки набора битовых флагов с помощью простого числа (обычно целого integer). Например, когда используется объект .NET System.Thread, Вам может понадобиться использовать свойство ThreadState. Это свойство имеет тип System.Threading.ThreadState, которое представлено типом enum (перечисление) с FlagsAttribute (атрибуты в виде флагов). Целочисленные значения enum являются степенями 2, потому что нам нужно использовать реальные биты целого числа, чтобы пометить различные состояния, см. [1]. Имейте в виду, что значения в ThreadState не являются взаимно исключающими (mutually exclusive), так что могут одновременно иметь место различные состояния флагов.
[Flags]
public enum ThreadState
{
Running = 0,
StopRequested = 1,
SuspendRequested = 2,
Background = 4,
Unstarted = 8,
Stopped = 16,
WaitSleepJoin = 32,
Suspended = 64,
AbortRequested = 128,
Aborted = 256
}
С числом Int32 у нас есть 32 бита, которые можно использовать как флаги. Целое число показывает состояния для обоих бит WaitSleepJoin и AbortRequested (если они имеют место) следующим образом:
Любая работа с битовыми полями происходит через битовые, поразрядные (bitwise) операции. Например, чтобы понять, в каком из двух интересующих нас состояний находится поток, мы можем сначала запомнить нужные нам биты:
t.ThreadState == (ThreadState.AbortRequested | ThreadState.WaitSleepJoin)
И затем проверить, установлен ли нужный бит:
t.ThreadState & ThreadState.AbortRequested) == ThreadState.AbortRequested
А вот так делается установка нужного бита (AbortRequested):
t.ThreadState |= ThreadState.AbortRequested
Что происходит, когда не используется FlagsAttribute? Рассмотрим следующее:
enum ColorsEnum
{
None = 0,
Red = 1,
Green = 2,
Blue = 4,
}
[Flags]
public enum ColorsFlaggedEnum
{
None = 0,
Red = 1,
Green = 2,
Blue = 4,
}
У нас есть два перечисления (enum) с одинаковыми значениями и с одинаковыми именами для строк, однако одно перечисление помечено атрибутом флага, а другое нет. Попробуем выполнить следующий код:
Console.WriteLine("Without Flags:");
for (int i = 0; i <= 8; i++)
{
Console.WriteLine("{0,3}: {1}", i, ((ColorsEnum)i).ToString());
}
Console.WriteLine("With Flags:");
for (int i = 0; i <= 8; i++)
{
Console.WriteLine("{0,3}: {1}", i, ((ColorsFlaggedEnum)i).ToString());
}
Запуск вышеуказанного кода произведет следующий вывод:
Without Flags: 0: None 1: Red 2: Green 3: 3 4: Blue 5: 5 6: 6 7: 7 8: 8
With Flags: 0: None 1: Red 2: Green 3: Red, Green 4: Blue 5: Red, Blue 6: Green, Blue 7: Red, Green, Blue 8: 8
Видно, что использование FlagsAttribute дает некий эффект. Однако нужно ли это на самом деле?
[Типы перечислений (enum)]
Перечисление enum не обязательно должно быть только типом integer, можно использовать любые встроенные типы (integral types) C# за исключением char. Цитата из документации c#:
"Можно использовать для enum типы byte, sbyte, short, ushort, int, uint, long или ulong."
Абсолютно нет никакой необходимости использовать целые числа со знаком (signed integer), это не доставляет проблем также если Вы работаете с битовыми операциями (bitwise operations). Нужно только использовать разрядность целого числа не более необходимого для хранения Ваших состояний. Например, если нужно обрабатывать 7 или меньшее количество состояний, то используйте тип byte (байт). Используйте Int16, если нужно хранить и обрабатывать 8 .. 15 состояний и так далее. Для состояния, когда все биты сброшены, используйте состояние "None" или что-нибудь похожее.
[Быстродействие (Performance)]
Самое большое преимущество битовых полей - выигрыш в быстродействии по сравнению с другими обычными методами (несколькими значениями типа boolean, словарями и т. д.). Увеличение быстродействия двойное. Во-первых, доступ к памяти. Если Вы используете Boolean для сохранении информации об объекте или любую другую информацию, то скорее всего Вы захотите устанавливать эти биты совместно. Часто пропускаемая оптимизация - кэш процессора. Вот код на ассемблере:
if (flag1 && flag2 && flag3)
0000011d cmp dword ptr [ebp-0Ch],0
00000121 je 0000013A
00000123 cmp dword ptr [ebp-10h],0
00000127 je 0000013A
00000129 cmp dword ptr [ebp-14h],0
0000012d je 0000013A
{
Console.WriteLine("true");
0000012f mov ecx,dword ptr ds:[03662088h]
00000135 call 049BD3E4
}
Console.ReadLine();
0000013a call 04F84F64
Когда CPU делает операцию cmp (сравнение), он пытается получить значения из кэша CPU, и если кэш не содержит нужные значения, то происходит обращение к памяти системы для загрузки значений в кэш. Когда данные не в кэше, то система испытывает потребность устранить неактуальность кэша (cache-miss). Основное правило вычисления времени доступа к данным - 10 циклов для данных в кэше, и 100 циклов для данных памяти вне кэша. Если Вам повезло (или если Вы очень осторожны), то все Ваши биты будут в той же самой линии памяти кэша. Линия кэша - это блок данных, выбранных из памяти в кэш CPU. В примере выше Вы можете получить в самом худшем случае до 3 неактуальностей кэша (cache-misses). Но если же Вы используете байт для этого примера как битовое поле, то все состояние будет находиться в кэше, и в самом худшем случае будет необходим только одни доступ к памяти. Современные CPU имеют 32..64 байтные линии кэша. Код на ассемблере:
if (myColor == (ColorsEnum.Blue | ColorsEnum.Green))
000000f3 cmp dword ptr [ebp-0Ch],6
000000f7 jne 00000104
{
Console.WriteLine("true");
000000f9 mov ecx,dword ptr ds:[032F2088h]
000000ff call 0485D3E4
}
Console.ReadLine();
00000104 call 04E24F64
Мы не только получили 2 инструкции вместо 6, мы также получили экономию на доступе к памяти. Эти числа будут удержаны для любого количества бит и для любого набора состояний этих бит, описанных в enum. Вывод: использование битовых полей - это очень круто.
[Ссылки]
1. Представление целых чисел в .NET. |