Мне было любопытно взглянуть на скомпилированный код, который создается, и я выяснил следующее о том, как наборы работают в Delphi 2010. Это объясняет, почему вы можете делать test := [8]
при test: set of 1..2
и почему Assert(8 in test)
завершается неудачно сразу после.
Сколько места фактически используется?
У set of byte
есть один бит для каждого возможного значения байта, всего 256 бит, 32 байта.Для set of 1..2
требуется 1 байт, но на удивление set of 100..101
также требуется один байт, поэтому компилятор Delphi довольно умён в распределении памяти.С другой стороны, set of 7..8
требует 2 байта и устанавливается на основе перечисления, которое включает в себя только значения 0
и 101
требует (задыхается) 13 байтов!
Тестовый код:
TTestEnumeration = (te0=0, te101=101);
TTestEnumeration2 = (tex58=58, tex101=101);
procedure Test;
var A: set of 1..2;
B: set of 7..8;
C: set of 100..101;
D: set of TTestEnumeration;
E: set of TTestEnumeration2;
begin
ShowMessage(IntToStr(SizeOf(A))); // => 1
ShowMessage(IntToStr(SizeOf(B))); // => 2
ShowMessage(IntToStr(SizeOf(C))); // => 1
ShowMessage(IntToStr(SizeOf(D))); // => 13
ShowMessage(IntToStr(SizeOf(E))); // => 6
end;
Выводы:
- Базовая модель набора -
set of byte
, с 256 возможными битами, 32 байта. - Delphi определяет необходимый непрерывный поддиапазониз общего диапазона 32 байта и использует это.Для случая
set of 1..2
он, вероятно, использует только первый байт, поэтому SizeOf()
возвращает 1. Для set of 100.101
он, вероятно, использует только 13-й байт, поэтому SizeOf()
возвращает 1. Для set of 7..8
он, вероятно, используетпервые два байта, поэтому мы получаем SizeOf()=2
.Это особенно интересный случай, потому что он показывает нам, что биты не сдвигаются ни влево, ни вправо для оптимизации хранения.Другой интересный случай - set of TTestEnumeration2
: он использует 6 байтов, даже там, где много неиспользуемых битов.
Какой код генерируется компилятором?
Тест 1, два набора, оба с использованием «первого байта».
procedure Test;
var A: set of 1..2;
B: set of 2..3;
begin
A := [1];
B := [1];
end;
Для тех, кто понимает Ассемблер, взгляните на сгенерированный код самостоятельно.Для тех, кто не понимает ассемблер, сгенерированный код эквивалентен:
begin
A := CompilerGeneratedArray[1];
B := CompilerGeneratedArray[1];
end;
И это не опечатка, компилятор использует одно и то же предварительно скомпилированное значение для обоих назначений.CompiledGeneratedArray[1] = 2
.
Вот еще один тест:
procedure Test2;
var A: set of 1..2;
B: set of 100..101;
begin
A := [1];
B := [1];
end;
Опять же, в псевдокоде, скомпилированный код выглядит так:
begin
A := CompilerGeneratedArray1[1];
B := CompilerGeneratedArray2[1];
end;
Опять нетопечатка: на этот раз компилятор использует разные предварительно скомпилированные значения для двух назначений.CompilerGeneratedArray1[1]=2
пока CompilerGeneratedArray2[1]=0
;Код, сгенерированный компилятором, достаточно умен, чтобы не перезаписывать биты в «B» недопустимыми значениями (потому что B содержит информацию о битах 96..103), но для обоих назначений используется очень похожий код.
Выводы
- Все операции набора работают отлично, если вы тестируете со значениями, которые находятся в базовом наборе.Для
set of 1..2
проверьте с 1
и 2
.Для теста set of 7..8
только с 7
и 8
.Я не считаю set
сломанным.Он отлично справляется со своей задачей во всем VCL (и в моем собственном коде это тоже есть). - По моему мнению, компилятор генерирует неоптимальный код для назначений множеств.Я не думаю, что поиск таблиц необходим, компилятор может генерировать значения в строке, и код будет иметь тот же размер, но лучшую локальность.
- Мое мнение таково, что побочный эффект от
set of 1..2
ведут себя так же, как set of 0..7
является побочным эффектом предыдущего отсутствия оптимизации в компиляторе. - В случае OP (
var test: set of 1..2; test := [7]
) компилятор должен выдать ошибку.Я бы не классифицировал это как ошибку, потому что я не думаю, что поведение компилятора должно определяться с точки зрения «что делать с плохим кодом программистом», а с точки зрения «что делать с хорошим кодом программистом»«;Тем не менее компилятор должен сгенерировать Constant expression violates subrange bounds
, как если бы вы попробовали этот код:
(пример кода)
procedure Test;
var t: 1..2;
begin
t := 3;
end;
- Во время выполнения, есликод скомпилирован с
{$R+}
, неправильное присвоение должно вызвать ошибку, как это происходит, если вы попробуете этот код:
(пример кода)
procedure Test;
var t: 1..2;
i: Integer;
begin
{$R+}
for i:=1 to 3 do
t := i;
{$R-}
end;