Что подтверждает утверждение о том, что C ++ может быть быстрее, чем JVM или CLR с JIT? [закрыто]

Повторяющаяся тема на SE, которую я заметил во многих вопросах, - это продолжающийся аргумент, что C ++ быстрее и /или эффективнее, чем языки более высокого уровня, такие как Java. Контр-аргумент заключается в том, что современные JVM или CLR могут быть столь же эффективными благодаря JIT и т. Д. Для растущего числа задач и что C ++ является только более эффективным, если вы знаете, что делаете и зачем делать что-то определенный способ заслуживает повышения производительности. Это очевидно и имеет смысл.

Я хотел бы узнать базовое объяснение (если есть такая вещь ...) относительно why и , как на C ++ быстрее выполняются определенные задачи, чем JVM или CLR? Это просто потому, что C ++ скомпилирован в машинный код, тогда как JVM или CLR все еще имеют служебную нагрузку на компиляцию JIT во время выполнения?

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

116 голосов | спросил Anonymous 3 PM00000060000002231 2012, 18:29:22

11 ответов


194

Это все о памяти (а не JIT). Преимущество JIT по сравнению с C в основном ограничивается оптимизацией виртуальных или не виртуальных вызовов с помощью inlining, то, что процессор BTB уже много работает.

В современных машинах доступ к ОЗУ действительно медленный (по сравнению с чем-либо, что делает процессор), что означает, что приложения, которые используют кеши как можно больше (что проще при использовании меньшей памяти), могут в сто раз быстрее , чем те, которые нет. И есть много способов, в которых Java использует больше памяти, чем C ++, и затрудняет запись приложений, полностью использующих кеш:

  • На каждый объект приходится накладные расходы не менее 8 байтов, а использование объектов вместо примитивов требуется или предпочтительнее во многих местах (а именно в стандартных коллекциях).
  • Строки состоят из двух объектов и имеют служебные данные из 38 байт .
  • UTF-16 используется внутри, что означает, что для каждого символа ASCII требуется два байта вместо одного (недавно JVM Oracle ввела оптимизацию, чтобы избежать этого для чистых строк ASCII).
  • Нет ссылочного типа совокупности (т. е. структур), и, в свою очередь, нет массивов ссылочных типов совокупности. Объект Java или массив объектов Java имеет очень плохую локальность кэша L1 /L2 по сравнению с C-структурами и массивами.
  • В Java-генераторах используется стирание типов, которое имеет плохую локальность кэша по сравнению с типом-экземпляром.
  • Распределение объектов непрозрачно и должно выполняться отдельно для каждого объекта, поэтому приложение не может преднамеренно выложить свои данные с помощью кэширования и по-прежнему рассматривать его как структурированные данные.

Некоторые другие факторы, связанные с памятью, но не связанные с кешем:

  • Не существует распределения стека, поэтому все не-примитивные данные, с которыми вы работаете, должны находиться в куче и проходить сборку мусора (некоторые JIT-серверы в некоторых случаях выполняют распределение стека за кулисами).
  • Поскольку не существует ссылочных типов совокупности, то нет стека передачи совокупных ссылочных типов. (Думайте эффективно пропустить векторные аргументы)
  • Сбор мусора может повредить содержимое кеша L1 /L2, а GC stop-the-world приостанавливает боль в интерактивности.
  • Преобразование между типами данных всегда требует копирования; вы не можете взять указатель на кучу байтов, полученных из сокета, и интерпретировать их как float.

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

Стоит отметить, что многие из этих компромиссов сильно отличаются для Java /JVM, чем для C # /CIL. .NET CIL имеет структуры ссылочного типа, распределение /передачу стека, упакованные массивы структур и генерируемые типом дженерики.

ответил Michael Borgwardt 3 PM00000070000001831 2012, 19:18:18
65
  

Это просто потому, что C ++ скомпилирован в сборку /машинный код   тогда как Java /C # все еще имеют накладные расходы на обработку JIT-компиляции   во время выполнения?

Частично, но в целом, предполагая абсолютно фантастический современный JIT-компилятор, правильный C ++-код еще имеет тенденцию работать лучше, чем Java-код, для двух основных причин:

1) Шаблоны C ++ предоставляют лучшие возможности для написания кода, который является общим И эффективным . Шаблоны предоставляют программисту на C ++ очень полезную абстракцию, которая имеет накладные расходы ZERO. (Шаблоны - это, в основном, компиляция с утиным типом.) Напротив, лучшее, что вы получаете в Java-генераторах, - это в основном виртуальные функции. У виртуальных функций всегда есть накладные расходы во время выполнения, и обычно они не могут быть встроены.

В целом, большинство языков, включая Java, C # и даже C, позволяют выбирать между эффективностью и общностью /абстракцией. Шаблоны C ++ дают вам обоим (за более длительное время компиляции.)

2) Тот факт, что стандарт C ++ мало что может сказать о бинарной компоновке скомпилированной C ++-программы, дает компиляторам C ++ гораздо больше возможностей, чем компилятор Java, что позволяет улучшить оптимизацию (за счет более сложных иногда отладка.) Фактически, сама природа спецификации языка Java обеспечивает ограничение производительности в определенных областях. Например, вы не можете иметь непрерывный массив объектов в Java. У вас может быть только непрерывный массив Object указателей (ссылки), что означает, что итерация по массиву в Java всегда берет на себя стоимость косвенности. Однако семантика значений C ++ позволяет создавать непрерывные массивы. Другое отличие состоит в том, что C ++ позволяет выделять объекты в стеке, тогда как Java не означает, что на практике, поскольку большинство программ на C ++ имеют тенденцию выделять объекты в стеке, стоимость выделения часто близка к нулю.

Одна область, где C ++ может отставать от Java, - это любая ситуация, когда в кучу нужно выделить много мелких объектов. В этом случае система сбора мусора Java, вероятно, приведет к лучшей производительности, чем стандартные new и delete в C ++, потому что Java GC позволяет осуществлять массовое освобождение. Но опять же, программист на C ++ может компенсировать это, используя пул памяти или распределитель slab, тогда как программист Java не имеет права обращаться к шаблону распределения памяти, для которого время выполнения Java не оптимизировано.

Кроме того, см. этот отличный ответ для получения дополнительной информации об этой теме.

ответил Charles Salvia 3 PM00000070000002531 2012, 19:02:25
46

Какие еще ответы (6 до сих пор), похоже, забыли упомянуть, но то, что я считаю очень важным для ответа на этот вопрос, является одной из основных концепций философии C ++, которая была сформулирована и применена Страуступом со дня № 1 :

Вы не платите за то, что не используете.

Есть и другие важные базовые принципы проектирования, которые сильно формировали C ++ (например, вы не должны быть принуждены к определенной парадигме), но Вы не платите за то, что вы не используете находится там среди самых важных.


В своей книге The Design and Evolution of C ++ (обычно называемой [D & E]), Страуструп описывает, что ему нужно, что заставило его придумать C ++ в первую очередь. Своими словами: для своей докторской диссертации (что-то делать с сетевыми симуляторами, IIRC), он реализовал систему в SIMULA, которой он очень любил, потому что язык был очень хорош, позволяя ему выражать свои мысли непосредственно в коде. Однако результирующая программа проходила слишком медленно, и, чтобы получить степень, он переписал эту вещь в BCPL, предшественнике C. Написав код в BCPL, он описывает как боль, но полученная программа была достаточно быстрой, чтобы доставить результаты, которые позволили ему закончить его PhD.

После этого он хотел, чтобы язык, который позволял бы переводить проблемы реального мира в код как можно более прямым образом, но также позволяет очень эффективный код.
В продолжение этого он создал то, что позже станет C ++.


Таким образом, приведенная выше цель не является лишь одним из нескольких основополагающих принципов дизайна, она очень близка к raison d'etre для C ++. И его можно найти примерно везде на языке: функции - это только virtual, когда вы хотите их (поскольку при вызове виртуальных функций возникают небольшие накладные расходы). POD инициализируются автоматически, когда вы явно запрашиваете это, исключения только стоят вашей производительности, когда вы на самом деле бросаете их (тогда как явная цель дизайна позволяла устанавливать /очищать стековые кадры очень дешево), никакой GC не запускается, когда бы он ни находился, и т. д.

C ++ явно решил не предоставлять вам некоторые удобства («должен ли я сделать этот метод виртуальным здесь?») в обмен на производительность («нет, я не знаю, и теперь компилятор может inline it и оптимизируйте черт из всего этого! »), и, что неудивительно, это действительно привело к увеличению производительности по сравнению с удобными языками.

ответил sbi 4 AM000000120000004631 2012, 00:45:46
29

Знаете ли вы исследовательский документ Google об этой теме?

Из заключения:

  

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

Это, по крайней мере, частично объяснение в смысле «потому что реальные компиляторы C ++ производят более быстрый код, чем компиляторы Java, посредством эмпирических мер».

ответил Doc Brown 3 PM00000070000004731 2012, 19:49:47
23

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

Подводя итог:

  

В сущности, семантика Java диктует, что она медленнее   языка, чем C ++.

Итак, в зависимости от того, с каким другим языком вы сравниваете C ++, вы можете получить или не тот же ответ.

В C ++ у вас есть:

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

Это функции или побочные эффекты определения языка, которые делают его теоретически более эффективным по памяти и скорости, чем любой язык, который:

  • массовое использование косвенно («все является управляемым справочным /указательным» языком): косвенное означает, что ЦП должен прыгать в памяти, чтобы получить необходимые данные, увеличивая отказы кэша ЦП, что означает замедление обработки - использование C также очень много ссылок, даже если он может иметь небольшие данные как C ++;
  • генерировать объекты большого размера, к которым обращаются косвенные члены: это следствие наличия ссылок по умолчанию, члены являются указателями, поэтому, когда вы получаете член, вы можете не получать данные близко к ядру родительского объекта, снова запуская промахи в кеше .
  • используйте сборщик garbarge: он просто делает предсказуемость производительности невозможной (по дизайну).

Агрессивная встраивание компилятора C ++ уменьшает или устраняет много ограничений. Способность генерировать небольшой набор компактных данных делает его кэшируемым, если вы не распространяете эти данные по всей памяти, а не вместе друг с другом (оба возможны, C ++ просто позволяет вам выбирать). RAII делает поведение памяти C ++ предсказуемым, устраняя множество проблем в случае моделирования в реальном времени или в режиме реального времени, которые требуют высокой скорости. Проблемы локальности, в общем, можно суммировать следующим образом: чем меньше программа /данные, тем быстрее выполняется выполнение. C ++ предоставляют различные способы убедиться, что ваши данные находятся там, где вы хотите (в пуле, массиве или т. Д.) И что он компактный.

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

ответил Klaim 3 PM00000060000002531 2012, 18:55:25
7

В основном это касается памяти (как сказал Майкл Боргвардт) с добавлением неэффективности JIT.

Одна вещь, о которой не упоминается, - это кеш, - чтобы полностью использовать кеш, вам нужно, чтобы ваши данные были размещены смежно (т.е. все вместе). Теперь с системой GC память выделяется на куче GC, которая быстрая, но по мере использования памяти GC будет регулярно пинать и удалять блоки, которые больше не используются, а затем сжать оставшиеся вместе. Теперь, помимо очевидной медлительности перемещения этих используемых блоков вместе, это означает, что данные, которые вы используете, могут не совпадать. Если у вас есть массив из 1000 элементов, если вы не выделили их сразу (а затем обновили их содержимое, а не удалили и создали новые, которые будут созданы в конце кучи), они будут разбросаны по всей куче, поэтому требуется несколько обращений к памяти, чтобы прочитать их все в кеш процессора. Приложение C /C ++ скорее всего выделит память для этих элементов, а затем вы обновите блоки данными. (хорошо, есть структуры данных, такие как список, который ведет себя больше как распределения памяти ГХ, но люди знают, что они медленнее, чем векторы).

Вы можете увидеть это в действии, просто заменив любые объекты StringBuilder String ... Stringbuilders работают, предварительно распределяя память и заполняя ее, и это известный трюк производительности для java /.NET-систем.

Не забывайте, что парадигма «удалить старые и выделять новые копии» очень сильно используется в Java /C #, просто потому, что людям говорят, что распределение памяти происходит очень быстро из-за GC, и поэтому модель рассеянной памяти получает используется везде (за исключением струнных строителей, конечно), поэтому все ваши библиотеки, как правило, расточительны по памяти и используют многие из них, ни одна из которых не приносит пользу смежности. Обвиняйте шумиху вокруг GC для этого - они сказали, что память свободна, lol.

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

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

Есть еще ... как загрузка сборок из GAC, требующая проверки безопасности, зондов-пробников (включите sxstrace и просто посмотрите, к чему это идет!) и общий другой пересмотр, который, кажется, намного более популярен в java /.net, чем C /C ++.

ответил gbjbaanb 4 AM00000030000002631 2012, 03:27:26
6

"Это просто потому, что C ++ скомпилирован в сборку /машинный код, тогда как Java /C # все еще имеют накладные расходы на обработку компиляции JIT во время выполнения?" В принципе, да!

Тем не менее, у Java больше накладных расходов, чем на компиляцию JIT. Например, он делает гораздо больше проверки для вас (так он делает такие вещи, как ArrayIndexOutOfBoundsExceptions и NullPointerExceptions). Сборщик мусора является еще одним значительным накладным капиталом.

Здесь представлено довольно подробное сравнение здесь .

ответил vaughandroid 3 PM00000060000004231 2012, 18:41:42
2

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

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

Предположим, что у нас есть программа, написанная на каком-то языке X, и мы можем скомпилировать ее с помощью собственного компилятора и снова с JIT-компилятором. Каждый рабочий поток имеет те же самые этапы, которые могут быть обобщены как (Код -> Промежуточное представление -> Машинный код -> Исполнение). Большая разница между двумя - это то, какие этапы видны пользователю и которые видят программисты. С собственной компиляцией программист видит все, кроме этапа выполнения, но с решением JIT компиляция для машинного кода рассматривается пользователем в дополнение к выполнению.

Утверждение о том, что A быстрее, чем B , относится к времени, затраченному на выполнение программы, , как видно пользователю . Если мы предположим, что обе части кода выполняются одинаково на этапе исполнения, мы должны предположить, что рабочий процесс JIT медленнее для пользователя, так как он также должен видеть время T компиляции для машинного кода, где T> 0. Таким образом, для любой возможности того, чтобы рабочий поток JIT выполнял то же самое, что и собственный рабочий поток, пользователю, мы должны сократить время выполнения кода, так что Исполнение + Компиляция для машинного кода меньше, чем только этап выполнения основного потока работ. Это означает, что мы должны лучше оптимизировать код в компиляции JIT, чем в исходной компиляции.

Это, однако, довольно неосуществимо, так как для выполнения необходимых оптимизаций для ускорения Execution мы должны тратить больше времени на этапе компиляции на машинный код, и, таким образом, любое время, которое мы сохраняем в результате оптимизированного кода, на самом деле потерянный, поскольку мы добавляем его в компиляцию. Другими словами, «медленность» решения на основе JIT происходит не только из-за дополнительного времени для компиляции JIT, но и для кода, созданного этой компиляцией, выполняется медленнее, чем собственное решение.

Я использую пример: распределение регистров. Поскольку доступ к памяти в несколько тысяч раз медленнее, чем доступ к регистру, мы в идеале хотим использовать регистры везде, где это возможно, и иметь как можно меньше возможностей доступа к памяти, но мы имеем ограниченное количество регистров, и мы должны разливать состояние в памяти, когда нам нужно регистр. Если мы используем алгоритм распределения регистров, который вычисляет 200 мс, и в результате мы сохраняем 2 мс времени выполнения - мы не используем время для JIT-компилятора. Такие решения, как алгоритм Чаитина, который может создавать высоко оптимизированный код, непригодны.

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

Но наши виртуальные машины не ограничиваются только компиляцией JIT. Они используют методы компиляции в режиме «сверху вниз», кэширование, «горячую» замену и адаптивную оптимизацию. Поэтому давайте изменим наше утверждение о том, что производительность - это то, что видит пользователь, и ограничивает время, затрачиваемое на выполнение программы (предположим, что мы собрали AOT). Мы можем эффективно сделать исполняемый код эквивалентным(или, может быть, лучше?). Большим требованием для виртуальных машин является то, что они могут создавать более качественный код, чем собственный компилятор, потому что он имеет доступ к дополнительной информации - о текущем процессе, например о том, как часто может выполняться определенная функция. Затем VM может применять адаптивные оптимизации к наиболее важному коду посредством горячей замены.

Проблема с этим аргументом заключается в том, что он предполагает, что оптимизация на основе профиля и тому подобное является чем-то уникальным для виртуальных машин, что неверно. Мы также можем применить его к встроенной компиляции - путем компиляции нашего приложения с включенным профилированием, записи информации и последующей перекомпиляции приложения с этим профилем. Вероятно, стоит также отметить, что горячая замена кода не является чем-то, что может сделать только компилятор JIT, мы можем сделать это для собственного кода, хотя решения для JIT для этого более доступны и намного проще для разработчика. Поэтому большой вопрос: может ли VM предложить нам некоторую информацию, которую не может создать собственная компиляция, что может повысить производительность нашего кода?

Я не вижу этого сам. Мы можем применить большинство методов типичной виртуальной машины к собственному коду тоже, хотя процесс более активно. Точно так же мы можем применить любые оптимизации встроенного компилятора к VM, которая использует компиляцию AOT или адаптивную оптимизацию. Реальность заключается в том, что разница между исходным кодом запуска и запуском в виртуальной машине не такая большая, как нам верили. В конечном итоге они приводят к одному и тому же результату, но они используют другой подход, чтобы добраться туда. VM использует итеративный подход для создания оптимизированного кода, где нативный компилятор ожидает его с самого начала (и его можно улучшить с помощью итеративного подхода).

Программист на C ++ может утверждать, что ему нужны оптимизации из get-go, и не следует ждать, пока виртуальная машина не выяснит, как их выполнять, если вообще. Вероятно, это актуально с нашей нынешней технологией, поскольку текущий уровень оптимизации в наших виртуальных машинах уступает тому, что могут предложить исходные компиляторы, но это может быть не всегда так, если улучшения AOT в наших виртуальных машинах улучшаются и т. Д.

ответил Mark H 5 AM00000070000002931 2012, 07:25:29
0

Эта статья является резюме набора сообщений в блоге, пытающихся сравнить скорость c ++ vs c # и проблем, которые вы должны преодолеть на обоих языках, чтобы получить высокопроизводительный код. Резюме: «Ваша библиотека важнее, чем что угодно, но если вы находитесь на c ++, вы можете это преодолеть». или «современные языки имеют лучшие библиотеки и, таким образом, получают более быстрые результаты с меньшими усилиями» в зависимости от вашего философского уклона.

ответил Jeff Gates 4 PM00000020000003431 2012, 14:47:34
0

Я думаю, что реальный вопрос здесь не «быстрее»? но «который имеет лучший потенциал для повышения производительности?». Рассматриваемый на этих условиях, C ++ явно выигрывает - он скомпилирован в собственный код, нет JITting, это более низкий уровень абстракции и т. Д.

Это далеко не полная история.

Поскольку C ++ компилируется, любая оптимизация компилятора должна выполняться во время компиляции, а оптимизация компилятора, подходящая для одной машины, может быть совершенно неправильной для другой. Кроме того, любая глобальная оптимизация компилятора может и будет поддерживать определенные алгоритмы или шаблоны кода над другими.

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

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

ответил Maximus Minimus 5 AM00000070000004431 2012, 07:21:44
-1

Компиляция JIT фактически отрицательно влияет на производительность. Если вы создадите «идеальный» компилятор и «идеальный» JIT-компилятор, первый вариант всегда будет выигрывать в производительности.

Оба Java и C # интерпретируются на промежуточные языки, а затем скомпилированы в собственный код во время выполнения, что снижает производительность.

Но теперь разница не очевидна для C #: Microsoft CLR создает другой собственный код для разных процессоров, что делает код более эффективным для работы компьютера, что не всегда выполняется компиляторами C ++.

P.S. C # написано очень эффективно и в нем не много слоев абстракции. Это не относится к Java, что не так эффективно. Таким образом, в этом случае, с его приветливым CLR, программы C # часто показывают лучшую производительность, чем программы на C ++. Подробнее о .Net и CLR см. «CLR через C #» Джеффри Рихтера .

ответил superM 3 PM00000060000003931 2012, 18:36:39

Похожие вопросы

Популярные теги

security × 330linux × 316macos × 2827 × 268performance × 244command-line × 241sql-server × 235joomla-3.x × 222java × 189c++ × 186windows × 180cisco × 168bash × 158c# × 142gmail × 139arduino-uno × 139javascript × 134ssh × 133seo × 132mysql × 132