Вопрос

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

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

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

Вот пример тестового приложения:


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 секунда, но почему бы и нет?

Это было полезно?

Решение

Ваш тест на самом деле несправедлив к версиям, основанным на свойствах.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());

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

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

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

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) 

Другие советы

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

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

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

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;
    }
}

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

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

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

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

У Рико Мариани есть пара связанных постов:

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

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

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

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

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

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

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

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

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

Вот несколько сценариев, в которых это нормально (из книги "Рекомендации по разработке фреймворка"):

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

И там , где его нет:

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

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

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

        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();

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

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

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

Здесь либо вам, либо вашему клиенту требуется производительность.В рамках вашего проекта ПРОИЗВОДИТЕЛЬНОСТЬ имеет РЕШАЮЩЕЕ значение, если я правильно понимаю.

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

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

Хорошего вам дня!

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

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top