Подтвердить что ты не робот

Являются ли публичные поля когда-либо в порядке?

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

Я работаю над довольно масштабным инженерным приложением, которое создает и работает с моделью памяти в структуре (что-нибудь от высотного здания до моста в сарай, не имеет значения). В этом проекте есть TON геометрического анализа и расчета. Чтобы поддержать это, модель состоит из множества крошечных неизменяемых структур только для чтения, чтобы представлять такие вещи, как точки, сегменты линий и т.д. Некоторым значениям этих структур (например, координатам точек) доступны десятки или сотни миллионов раз во время типичного выполнения программы. Из-за сложности моделей и объема вычислений производительность абсолютно критическая.

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

Здесь пример тестового приложения:


struct Point {
    public Point(double x, double y, double z) {
        _x = x;
        _y = y;
        _z = z;
    }

    public readonly double _x;
    public readonly double _y;
    public readonly double _z;

    public double X { get { return _x; } }
    public double Y { get { return _y; } }
    public double Z { get { return _z; } }
}

class Program {
    static void Main(string[] args) {
        const int loopCount = 10000000;

        var point = new Point(12.0, 123.5, 0.123);

        var sw = new Stopwatch();
        double x, y, z;
        double calculatedValue;
        sw.Start();
        for (int i = 0; i < loopCount; i++) {
            x = point._x;
            y = point._y;
            z = point._z;
            calculatedValue = point._x * point._y / point._z;
        }
        sw.Stop();
        double fieldTime = sw.ElapsedMilliseconds;
        Console.WriteLine("Direct field access: " + fieldTime);

        sw.Reset();
        sw.Start();
        for (int i = 0; i < loopCount; i++) {
            x = point.X;
            y = point.Y;
            z = point.Z;
            calculatedValue = point.X * point.Y / point.Z;
        }
        sw.Stop();
        double propertyTime = sw.ElapsedMilliseconds;
        Console.WriteLine("Property access: " + propertyTime);

        double totalDiff = propertyTime - fieldTime;
        Console.WriteLine("Total difference: " + totalDiff);
        double averageDiff = totalDiff / loopCount;
        Console.WriteLine("Average difference: " + averageDiff);

        Console.ReadLine();
    }
}

результат:
Прямой доступ к полям: 3262
Доступ к объекту: 24248
Общая разница: 20986
Средняя разница: 0.00020986


Это всего лишь 21 секунда, но почему бы и нет?

4b9b3361

Ответ 1

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

В вашем примере все тело цикла версии доступа к полю оптимизировано, просто:

for (int i = 0; i < loopCount; i++)
00000025  xor         eax,eax 
00000027  inc         eax  
00000028  cmp         eax,989680h 
0000002d  jl          00000027 
}

тогда как вторая версия фактически выполняет деление с плавающей запятой на каждой итерации:

for (int i = 0; i < loopCount; i++)
00000094  xor         eax,eax 
00000096  fld         dword ptr ds:[01300210h] 
0000009c  fdiv        qword ptr ds:[01300218h] 
000000a2  fstp        st(0) 
000000a4  inc         eax  
000000a5  cmp         eax,989680h 
000000aa  jl          00000096 
}

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

Сначала рандомизируйте входные значения так, чтобы они не были константами, а JIT недостаточно умен, чтобы полностью удалить разделение.

Изменить:

Point point = new Point(12.0, 123.5, 0.123);

в

Random r = new Random();
Point point = new Point(r.NextDouble(), r.NextDouble(), r.NextDouble());

Во-вторых, убедитесь, что результаты каждой итерации цикла используются где-то:

Перед каждым циклом установите calculateValue = 0, чтобы они оба начинались с одной и той же точки. После каждого цикла вызовите Console.WriteLine(calculateValue.ToString()), чтобы убедиться, что результат "используется", поэтому компилятор не оптимизирует его. Наконец, измените тело цикла с "calculateValue =..." на "calculateValue + =...", чтобы использовать каждую итерацию.

На моей машине эти изменения (с выпуском сборки) дают следующие результаты:

Direct field access: 133
Property access: 133
Total difference: 0
Average difference: 0

Как и ожидалось, x86 для каждой из этих модифицированных циклов идентичен (кроме адреса цикла)

000000dd  xor         eax,eax 
000000df  fld         qword ptr [esp+20h] 
000000e3  fmul        qword ptr [esp+28h] 
000000e7  fdiv        qword ptr [esp+30h] 
000000eb  fstp        st(0) 
000000ed  inc         eax  
000000ee  cmp         eax,989680h 
000000f3  jl          000000DF (This loop address is the only difference) 

Ответ 2

Учитывая, что вы имеете дело с неизменяемыми объектами с полями readonly, я бы сказал, что вы попали в один случай, когда я не считаю, что публичные поля являются грязной привычкой.

Ответ 3

IMO, правило "без публичных полей" является одним из тех правил, которые являются технически правильными, но если вы не проектируете библиотеку, предназначенную для использования общественностью, это вряд ли вызовет у вас какую-либо проблему, если вы ее сломаете.

Прежде чем я получу слишком широкое распространение, я должен добавить, что инкапсуляция - это хорошо. Учитывая инвариант, свойство Value должно быть нулевым, если HasValue является ложным, этот дизайн является ошибочным:

class A {
    public bool HasValue;
    public object Value;
}

Однако, учитывая этот инвариант, эта конструкция одинаково ошибочна:

class A {
    public bool HasValue { get; set; }
    public object Value { get; set; }
}

Правильный дизайн

class A {
    public bool HasValue { get; private set; }
    public object Value { get; private set; }

    public void SetValue(bool hasValue, object value) {
        if (!hasValue && value != null)
            throw new ArgumentException();
        this.HasValue = hasValue;
        this.Value    = value;
    }
}

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

Ответ 4

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

Мораль истории - это может противоречить всему, чему вас учили или советовали, но тесты не лгут. Если он работает лучше, просто сделайте это.

Ответ 5

Если вам действительно нужна дополнительная производительность, то, вероятно, это правильно. Если вам не нужна дополнительная производительность, возможно, это не так.

У Rico Mariani есть несколько связанных должностей:

Ответ 6

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

В других случаях это просто слишком "неправильно", чтобы сделать это.

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

Ответ 7

Попробуйте компилировать сборку релиза и запускать непосредственно из exe, а не через отладчик. Если приложение было запущено через отладчик, тогда компилятор JIT не включит аксессоров свойств. Я не смог воспроизвести ваши результаты. Фактически, каждое проведенное тестирование показало, что практически не было разницы во времени выполнения.

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

Изменить: Хорошо, мои начальные тесты использовали тип данных int вместо double. Я вижу огромную разницу при использовании парных. С ints прямая vs. свойство практически одинакова. При удвоении доступа к ресурсам примерно на 7 раз медленнее, чем прямой доступ к моей машине. Это несколько озадачивает меня.

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

Ответ 8

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

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

Ответ 9

Здесь приведены некоторые сценарии, где все в порядке (из книги "Руководства по дизайну Framework" ):

  • DO использовать постоянные поля для констант это никогда не изменится.
  • Используйте общедоступные статические поля readonly для предопределенных объектов.

И где это не так:

  • НЕ назначайте экземпляры изменчивых типы для полей только для чтения.

Из того, что вы заявили, я не понимаю, почему ваши тривиальные свойства не попадают в JIT?

Ответ 10

Если вы измените свой тест на использование временных переменных, которые вы назначаете, а не напрямую получаете доступ к свойствам в вашем расчете, вы увидите значительное улучшение производительности:

        sw.Start();
        for (int i = 0; i < loopCount; i++)
        {
            x = point._x;
            y = point._y;
            z = point._z;
            calculatedValue = x * y / z;
        }
        sw.Stop();
        double fieldTime = sw.ElapsedMilliseconds;
        Console.WriteLine("Direct field access: " + fieldTime);

        sw.Reset();
        sw.Start();
        for (int i = 0; i < loopCount; i++)
        {
            x = point.X;
            y = point.Y;
            z = point.Z;
            calculatedValue = x * y / z;
        }
        sw.Stop();

Ответ 11

Возможно, я повторю кого-то еще, но здесь тоже моя точка зрения, если это может помочь.

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

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

Здесь вам или вашему клиенту требуется производительность. В рамках вашего проекта PERFORMANCE CRUCIAL, если я правильно понимаю.

Итак, я думаю, вы согласитесь со мной, что нас не волнует, как выглядит код или уважает ли он "искусство". Сделайте то, что вам нужно, чтобы сделать его мощным и мощным! Свойства позволяют вашему коду "форматировать" ввод данных в случае необходимости. Свойство имеет свой собственный адрес памяти, затем он ищет свой адрес-член, когда вы возвращаете значение члена, поэтому вы получили два запроса адреса. Если производительность такая критическая, просто сделайте это и сделайте своих неизменных участников общедоступными.: -)

Это отражает и некоторые другие точки зрения, если я правильно прочитаю.:)

Хорошего дня!

Ответ 12

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