Я работаю в .Net, C # shop, и у меня есть коллега, который настаивает на том, чтобы мы использовали гигантские операторы Switch в нашем коде с большим количеством «падежей», а не с более объектно-ориентированными подходами. Его аргумент последовательно восходит к тому факту, что оператор Switch компилируется в «таблицу переходов процессора» и, следовательно, является самым быстрым вариантом (хотя в других случаях нашей команде говорят, что нас не волнует скорость).
У меня, честно говоря, нет аргументов против этого ... потому что я не знаю, какого черта он говорит.
Он прав?
Он просто говорит свою задницу?
Просто пытаюсь учиться здесь.
c#
.net
switch-statement
Джеймс П. Райт
источник
источник
Ответы:
Он, вероятно, старый хакер C и да, он говорит из своей задницы. .Net - это не C ++; компилятор .Net продолжает совершенствоваться, и большинство умных взломов приводят к обратным результатам, если не сегодня, то в следующей версии .Net. Небольшие функции предпочтительнее, потому что .Net JIT-ы каждую функцию один раз перед ее использованием. Таким образом, если некоторые случаи никогда не будут затронуты во время жизненного цикла программы, то при их компиляции JIT не будет никаких затрат. Во всяком случае, если скорость не является проблемой, не должно быть оптимизаций. Сначала напишу для программиста, потом для компилятора. Ваш коллега не будет легко убедить, поэтому я бы эмпирически доказал, что лучше организованный код на самом деле быстрее. Я выбрал бы один из его худших примеров, переписал бы их лучше, а затем убедился, что ваш код работает быстрее. Вишня, если нужно. Затем запустите его несколько миллионов раз, профилируйте и покажите ему.
РЕДАКТИРОВАТЬ
Билл Вагнер написал:
Пункт 11: Понимание привлекательности небольших функций (эффективное издание C #, второе издание) Помните, что перевод кода C # в машинно-исполняемый код - это двухэтапный процесс. Компилятор C # генерирует IL, который доставляется в сборках. При необходимости JIT-компилятор генерирует машинный код для каждого метода (или группы методов, если используется встраивание). Небольшие функции значительно упрощают амортизацию JIT-компилятором. Небольшие функции также с большей вероятностью будут кандидатами на встраивание. Это не просто малость: более простой поток управления имеет такое же значение. Меньшее количество ветвей управления внутри функций облегчает JIT-компилятору регистрацию переменных. Написание более понятного кода - не просто хорошая практика; это то, как вы создаете более эффективный код во время выполнения.
EDIT2:
Итак ... очевидно, оператор switch быстрее и лучше, чем набор операторов if / else, потому что одно сравнение является логарифмическим, а другое - линейным. http://sequence-points.blogspot.com/2007/10/why-is-switch-statement-faster-than-if.html
Что ж, мой любимый подход к замене огромного оператора switch - это словарь (или иногда даже массив, если я включаю перечисления или маленькие целые числа), который отображает значения в функции, которые вызываются в ответ на них. Это вынуждает убрать много неприятного общего состояния спагетти, но это хорошо. Большое заявление переключателя обычно - кошмар обслуживания. Итак ... с массивами и словарями поиск будет занимать постоянное время, и при этом будет мало дополнительной памяти.
Я все еще не убежден, что заявление о переключении лучше.
источник
Если ваш коллега не может предоставить доказательство того, что это изменение дает реальное измеримое преимущество в масштабе всего приложения, оно уступает вашему подходу (т. Е. Полиморфизму), который фактически обеспечивает такое преимущество: ремонтопригодность.
Микрооптимизация должна проводиться только после устранения узких мест. Преждевременная оптимизация - корень всего зла .
Скорость поддается количественной оценке. Там мало полезной информации в «подход A быстрее, чем подход B». Вопрос «На сколько быстрее? ».
источник
Кого волнует, если это быстрее?
Если вы не пишете программное обеспечение для реального времени, маловероятно, что незначительное ускорение, которое вы могли бы получить, совершая что-то совершенно безумным образом, будет иметь большое значение для вашего клиента. Я бы даже не стал сражаться с этим на скорости, этот парень явно не собирается прислушиваться к каким-либо аргументам на эту тему.
Однако, ремонтопригодность - цель игры, а гигантский оператор switch даже немного не поддается обслуживанию. Как вы объясните различные пути в коде новым парням? Документация должна быть такой же, как и сам код!
Кроме того, вы получаете полную неспособность эффективно выполнять модульное тестирование (слишком много возможных путей, не говоря уже о возможном отсутствии интерфейсов и т. Д.), Что делает ваш код еще менее поддерживаемым.
[Что касается заинтересованности: JITter лучше работает на меньших методах, поэтому гигантские операторы switch (и их изначально большие методы) повредят вашей скорости в больших сборках, IIRC.]
источник
Отойди от оператора switch ...
Этот тип оператора switch следует избегать, как чумы, потому что он нарушает принцип Open Closed . Это заставляет команду вносить изменения в существующий код, когда необходимо добавить новые функциональные возможности, а не просто добавлять новый код.
источник
Я пережил кошмар, известный как массивный конечный автомат, которым манипулируют массивные операторы переключения. Еще хуже, в моем случае, FSM охватывает три библиотеки C ++, и было совершенно ясно, что код был написан кем-то, кто разбирается в C.
Метрики, о которых вам нужно заботиться:
Мне было поручено добавить новую функцию в этот набор библиотек DLL, и я смог убедить руководство в том, что мне потребуется столько же времени, чтобы переписать 3 библиотеки DLL в одну правильно ориентированную на объект библиотеку DLL, так же, как для меня было бы исправление обезьяны. и жюри подгоняют решение к тому, что уже было там. Переписывание прошло с огромным успехом, поскольку оно не только поддерживало новую функциональность, но и было намного легче расширять. Фактически, задача, которая обычно занимает неделю, чтобы убедиться, что вы ничего не сломали, может занять несколько часов.
Так как насчет времени выполнения? Не было никакого увеличения или уменьшения скорости. Чтобы быть справедливым, наша производительность была ограничена системными драйверами, поэтому, если бы объектно-ориентированное решение было на самом деле медленнее, мы бы этого не знали.
Что не так с массивными операторами switch для языка OO?
источник
Я не покупаю аргумент производительности; это все о поддержке кода.
НО: иногда гигантский оператор switch легче поддерживать (меньше кода), чем группа небольших классов, переопределяющих виртуальную функцию (и) абстрактного базового класса. Например, если бы вы реализовали эмулятор ЦП, вы бы не реализовали функциональность каждой инструкции в отдельном классе - вы просто поместили бы ее в гигантский swtich в коде операции, возможно, вызывая вспомогательные функции для более сложных инструкций.
Практическое правило: если переключение выполняется для ТИПА, вам, вероятно, следует использовать наследование и виртуальные функции. Если переключение выполняется на ЗНАЧЕНИЕ фиксированного типа (например, код операции инструкции, как указано выше), можно оставить все как есть.
источник
Вы не можете убедить меня в том, что:
Значительно быстрее чем:
Кроме того, версия OO более удобна в обслуживании.
источник
Он прав, что полученный машинный код, вероятно, будет более эффективным. Компилятор существенно преобразует оператор switch в набор тестов и ветвей, в которых будет относительно мало инструкций. Существует высокая вероятность того, что код, полученный в результате более абстрактных подходов, потребует большего количества инструкций.
ОДНАКО : Это почти наверняка тот случай, когда вашему конкретному приложению не нужно беспокоиться о такого рода микрооптимизации, иначе вы бы не использовали .net в первую очередь. Для чего-либо, кроме очень ограниченных встроенных приложений или интенсивной работы процессора, вы всегда должны позволить компилятору заниматься оптимизацией. Сконцентрируйтесь на написании чистого, поддерживаемого кода. Это почти всегда имеет гораздо большее значение, чем несколько десятых наносекунды во время выполнения.
источник
Одна из основных причин использования классов вместо операторов switch состоит в том, что операторы switch имеют тенденцию приводить к одному огромному файлу, который имеет много логики. Это одновременно и кошмар обслуживания, и проблема с управлением исходным кодом, поскольку вам нужно проверить и отредактировать этот огромный файл вместо файлов меньшего класса
источник
оператор switch в ООП-коде убедительно указывает на отсутствие классов
попробуйте оба способа и выполните несколько простых тестов скорости; скорее всего, разница не значительна. Если они есть, а код критичен ко времени, сохраните оператор switch
источник
Обычно я ненавижу слово «преждевременная оптимизация», но это пахнет им. Стоит отметить, что Кнут использовал эту знаменитую цитату в контексте использования
goto
операторов для ускорения работы кода в критических областях. Это ключ: критические пути.Он предлагал использовать
goto
для ускорения кода, но предостерегал против тех программистов, которые хотели бы делать такие вещи, основываясь на догадках и суевериях для кода, который даже не критичен.Одностороннее предпочтение
switch
выражений как можно более равномерно по всей базе кода (независимо от того, обрабатывается ли большая нагрузка) - классический пример того, что Кнут называет «программистом и безумным» программистом, который целый день изо всех сил старается сохранить их «оптимизированными». "код, который превратился в отладочный кошмар в результате попытки сэкономить копейки за фунты. Такой код редко обслуживаем, не говоря уже о том, что он эффективен.Он прав с точки зрения эффективности. Ни один компилятор, насколько мне известно, не может оптимизировать полиморфный код, включающий объекты и динамическую диспетчеризацию, лучше, чем оператор switch. Вы никогда не получите LUT или таблицу перехода к встроенному коду из полиморфного кода, поскольку такой код имеет тенденцию служить барьером оптимизатора для компилятора (он не будет знать, какую функцию вызывать до момента, когда происходит динамическая диспетчеризация). происходит).
Более полезно не думать об этой стоимости с точки зрения таблиц переходов, а с точки зрения барьера оптимизации. Для полиморфизма вызов
Base.method()
не позволяет компилятору знать, какая функция в действительности будет вызвана, еслиmethod
она виртуальная, не запечатана и может быть переопределена. Поскольку он не знает, какая функция на самом деле будет вызываться заранее, он не может оптимизировать вызов функции и использовать больше информации при принятии решений по оптимизации, поскольку на самом деле он не знает, какая функция будет вызываться при время компиляции кода.Оптимизаторы работают лучше всего, когда они могут вглядываться в вызов функции и делать оптимизации, которые либо полностью выравнивают вызывающего и вызываемого абонентов, либо, по крайней мере, оптимизируют вызывающего абонента для наиболее эффективной работы с вызываемым абонентом. Они не могут этого сделать, если не знают, какая функция будет вызываться заранее.
Использование этой стоимости, которая часто составляет копейки, чтобы оправдать превращение ее в стандарт кодирования, применяемый единообразно, как правило, очень глупо, особенно для мест, где есть потребность в расширяемости. Это главное, на что вы должны обращать внимание с настоящими преждевременными оптимизаторами: они хотят превратить незначительные проблемы с производительностью в стандарты кодирования, применяемые единообразно во всей кодовой базе, не обращая внимания на удобство сопровождения.
Я немного обижаюсь на цитату «старого хакера», использованную в принятом ответе, поскольку я один из них. Не каждый, кто десятилетиями занимался кодированием, начиная с очень ограниченного аппаратного обеспечения, превратился в преждевременного оптимизатора. Тем не менее, я столкнулся и работал с ними тоже. Но эти типы никогда не измеряют такие вещи, как неправильное предсказание ветвлений или ошибки кэширования, они думают, что знают лучше, и основывают свои представления о неэффективности на сложной производственной кодовой базе, основанной на суевериях, которые сегодня не верны, а иногда и никогда не сбываются. Люди, которые по-настоящему работали в областях, критичных к производительности, часто понимают, что эффективная оптимизация - это эффективная расстановка приоритетов, а попытка обобщить стандарт кодирования, ухудшающий ремонтопригодность, для экономии копеек - очень неэффективная расстановка приоритетов.
Копейки важны, когда у вас есть дешевая функция, которая не выполняет так много работы, которая вызывается миллиард раз в очень узкой, критичной для производительности петле. В этом случае мы сэкономим 10 миллионов долларов. Брать копейки не стоит, если у вас есть функция, вызываемая дважды, для которой одно тело стоит тысячи долларов. Не стоит тратить время на то, чтобы тратить время на копейки во время покупки автомобиля. Стоит поторговаться за копейки, если вы покупаете миллион банок содовой у производителя. Ключом к эффективной оптимизации является понимание этих затрат в их правильном контексте. Кто-то, кто пытается сэкономить копейки на каждой покупке и предлагает, чтобы все остальные пытались торговать копейки независимо от того, что они покупают, не является опытным оптимизатором.
источник
Похоже, ваш коллега очень обеспокоен производительностью. Возможно, в некоторых случаях большая структура case / switch будет работать быстрее, но, надеюсь, вы, ребята, проведете эксперимент, выполнив временные тесты для версии OO и версии switch / case. Я предполагаю, что версия OO содержит меньше кода и ее легче отслеживать, понимать и поддерживать. Сначала я бы поспорил с версией OO (так как обслуживание / удобочитаемость должна быть изначально более важной), и рассматривал бы версию коммутатора / корпуса только в том случае, если версия OO имеет серьезные проблемы с производительностью, и можно показать, что коммутатор / корпус создаст значительное улучшение.
источник
Одно из преимуществ удобства сопровождения полиморфизма, о котором никто не упомянул, состоит в том, что вы сможете намного лучше структурировать свой код, используя наследование, если вы всегда переключаетесь на один и тот же список случаев, но иногда несколько случаев обрабатываются одинаково, а иногда они не
Например. если вы переключаетесь между
Dog
,Cat
иElephant
, а иногдаDog
иCat
имеете один и тот же случай, вы можете сделать так, чтобы они оба наследовали от абстрактного классаDomesticAnimal
и поместили эти функции в абстрактный класс.Кроме того, я был удивлен, что несколько человек использовали парсер как пример того, где вы не будете использовать полиморфизм. Для древовидного синтаксического анализатора это определенно неправильный подход, но если у вас есть что-то вроде сборки, где каждая строка несколько независима и начинается с кода операции, который указывает, как должна интерпретироваться остальная часть строки, я бы полностью использовал полиморфизм и завод. Каждый класс может реализовывать функции вроде
ExtractConstants
илиExtractSymbols
. Я использовал этот подход для игрушечного интерпретатора BASIC.источник
«Мы должны забыть о малой эффективности, скажем, в 97% случаев: преждевременная оптимизация - корень всего зла»
Дональд Кнут
источник
Даже если это не плохо для удобства обслуживания, я не верю, что это будет лучше для производительности. Вызов виртуальной функции - это просто одна дополнительная косвенность (такая же, как в лучшем случае для оператора switch), поэтому даже в C ++ производительность должна быть примерно одинаковой. В C #, где все вызовы функций являются виртуальными, оператор switch должен быть хуже, поскольку у вас одинаковые издержки вызова виртуальных функций в обеих версиях.
источник
Ваш коллега говорит не из-за своей задней стороны, насколько комментарии касаются таблиц прыжков. Однако, используя это, чтобы оправдать написание плохого кода, он ошибается.
Компилятор C # преобразует операторы switch с несколькими случаями в серию if / else, так что это не быстрее, чем использование if / else. Компилятор преобразует более крупные операторы switch в словарь (таблицу переходов, на которую ссылается ваш коллега). Пожалуйста, смотрите этот ответ на вопрос переполнения стека по теме для более подробной информации .
Большой оператор switch трудно читать и поддерживать. Словарь "падежей" и функций намного удобнее для чтения. Так как это то, во что превращается переключатель, вам и вашему коллеге было бы неплохо использовать словари напрямую.
источник
Он не обязательно говорит из своей задницы. По крайней мере, в C и C ++
switch
операторы могут быть оптимизированы для перехода по таблицам, хотя я никогда не видел, чтобы это происходило с динамической диспетчеризацией в функции, которая имеет доступ только к базовому указателю. По крайней мере, последний требует гораздо более умного оптимизатора, который смотрит на гораздо больше окружающего кода, чтобы точно определить, какой подтип используется из вызова виртуальной функции через базовый указатель / ссылку.Вдобавок к этому динамическая диспетчеризация часто служит «барьером оптимизации», то есть компилятор часто не сможет встроить код и оптимально распределить регистры, чтобы минимизировать потери стека и все такое причудливое, поскольку он не может понять, что Виртуальная функция будет вызываться через базовый указатель, чтобы встроить ее и выполнить всю магию оптимизации. Я не уверен, что вы даже хотите, чтобы оптимизатор был таким умным и пытался оптимизировать непрямые вызовы функций, поскольку это может привести к тому, что многие ветви кода придется генерировать отдельно в заданном стеке вызовов (функция, вызов которой
foo->f()
будет иметь генерировать совершенно другой машинный код от того, который вызываетbar->f()
через базовый указатель, и функция, вызывающая эту функцию, должна будет генерировать две или более версии кода и т. д. - объем генерируемого машинного кода будет взрывоопасным - возможно, не так уж плохо с трассировкой JIT, которая генерирует код на лету, поскольку он отслеживает горячие пути выполнения).Однако, как многие из ответов повторили, это плохая причина, чтобы одобрить кучу
switch
утверждений, даже если это происходит быстрее на некоторое количество. Кроме того, когда речь идет о микроэффективности, такие вещи, как ветвление и встраивание, обычно имеют довольно низкий приоритет по сравнению с такими вещами, как шаблоны доступа к памяти.Тем не менее, я прыгнул сюда с необычным ответом. Я хочу обосновать возможность сопровождения
switch
операторов над полиморфным решением тогда и только тогда, когда вы точно знаете, что будет только одно место, которое необходимо выполнитьswitch
.Ярким примером является центральный обработчик событий. В этом случае у вас обычно не так много мест, где обрабатываются события, только одно (почему оно «центральное»). В этих случаях вам не выгодна расширяемость, которую обеспечивает полиморфное решение. Полиморфное решение выгодно, когда есть много мест, где можно сделать аналогичное
switch
утверждение. Если вы точно знаете, что будет только один,switch
оператор с 15 случаями может быть намного проще, чем проектирование базового класса, унаследованного 15 подтипами, с переопределенными функциями и фабрикой для их создания, только для последующего использования в одной функции. во всей системе. В этих случаях добавление нового подтипа намного сложнее, чем добавлениеcase
оператора в одну функцию. Во всяком случае, я бы поспорил за ремонтопригодность, а не производительность,switch
заявления в этом особом случае, когда вы не получаете никакой пользы от расширяемости.источник