Открытые поля когда-нибудь в порядке? - PullRequest
22 голосов
/ 11 сентября 2009

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

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

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

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


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

Ответы [ 12 ]

31 голосов
/ 11 сентября 2009

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

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

Перед каждым циклом, рассчитать значение = 0, чтобы они оба начинались в одной и той же точке. После каждого цикла вызывайте Console.WriteLine (вычисленное значение.ТoString ()), чтобы убедиться, что результат «используется», чтобы компилятор не оптимизировал его. И, наконец, измените тело цикла с «selectedValue = ...» на «selectedValue + = ...», чтобы использовать каждую итерацию.

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

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) 
21 голосов
/ 11 сентября 2009

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

10 голосов
/ 11 сентября 2009

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

Прежде чем я получу слишком низкое голосование, я должен добавить, что инкапсуляция - хорошая вещь. Учитывая, что инвариант «свойство 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;
    }
}

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

3 голосов
/ 11 сентября 2009

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

Rico Mariani имеет пару связанных сообщений:

3 голосов
/ 11 сентября 2009

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

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

1 голос
/ 11 сентября 2009

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

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

1 голос
/ 11 сентября 2009

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

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

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

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

1 голос
/ 11 сентября 2009

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

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

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

0 голосов
/ 14 сентября 2012

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

0 голосов
/ 11 сентября 2009

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

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

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

Здесь вам или вашему клиенту требуется производительность. В рамках вашего проекта, если я правильно понимаю, ЭФФЕКТИВНОСТЬ НЕОБХОДИМА.

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

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

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...