Windows C Runtime медленный, когда задан языковой стандарт
Я диагностирую крайний случай в кроссплатформенном (Windows и Linux) приложении, где toupper значительно медленнее в Windows. Я предполагаю, что это то же самое и для tolower.
Первоначально я проверял это с помощью простой программы на C для каждой из них без набора информации о локали или даже без заголовочного файла, и разница в производительности была очень незначительной. Test был миллионом итерационных циклов, вызывающих каждый символ для строки в функции toupper().
После включения файла заголовка и включения строки ниже он становится намного медленнее и вызывает много специфических функций локали библиотеки времени выполнения MS C. Это хорошо, но производительность очень плохая. В Linux это никак не влияет на производительность.
setlocale(LC_ALL, ""); // system default locale
Если я установлю следующее, он будет работать так же быстро, как Linux, но, похоже, пропускает все функции локали.
setlocale(LC_ALL, NULL); // should be interpreted as the same as below?
OR
setlocale(LC_ALL, "C");
Примечание: Visual Studio 2015 для Windows 10 G++ для Linux под управлением Cent OS
Пробовал голландские настройки, настройки и тот же результат, медленно на Windows нет разницы в скорости на Linux.
Я делаю что-то не так или есть ошибка с настройками локали в Windows, или это другой путь, когда Linux не делает то, что должен? Я не выполнял отладку приложения linux, так как я не так хорошо знаком с linux, поэтому не знаю точно, что он делает внутри. Что я должен проверить дальше, чтобы разобраться?
Код ниже для тестирования (Linux):
// C++ is only used for timing. The original program is in C.
#include <stdio.h>
#include <stdlib.h>
#include <ctype.h>
#include <chrono>
#include <locale.h>
using namespace std::chrono;
void strToUpper(char *strVal);
int main()
{
typedef high_resolution_clock Clock;
high_resolution_clock::time_point t1 = Clock::now();
// set locale
//setlocale(LC_ALL,"nl_NL");
setlocale(LC_ALL,"en_US");
// testing string
char str[] = "the quick brown fox jumps over the lazy dog";
for (int i = 0; i < 1000000; i++)
{
strToUpper(str);
}
high_resolution_clock::time_point t2 = Clock::now();
duration<double> time_span = duration_cast<duration<double>>(t2 - t1);
printf("chrono time %2.6f:\n",time_span.count());
}
void strToUpper(char *strVal)
{
unsigned char *t;
t = (unsigned char *)strVal;
while (*t)
{
*t = toupper(*t);
*t++;
}
}
Для окон измените локальную информацию на:
// set locale
//setlocale(LC_ALL,"nld_nld");
setlocale(LC_ALL, "english_us");
Вы можете увидеть изменение локали из разделителя по истечении времени, полная остановка или запятая.
РЕДАКТИРОВАТЬ - Профилирование данных Как вы можете видеть выше, большую часть времени тратится на дочерние системные вызовы от _toupper_l. Без установленной информации о локализации вызов toupper НЕ вызывает дочерний _toupper_l, что делает его очень быстрым.
1 ответ
Одинаковая (и довольно хорошая) производительность при использовании LANG=C и LANG = чего-либо еще ожидается для реализации glibc, используемой в Linux.
Ваши результаты по Linux имеют смысл. Ваш метод тестирования, вероятно, в порядке. Используйте профилировщик, чтобы узнать, сколько времени ваш микробенчмарк проводит внутри функций Windows. Если реализация Windows оказывается проблемой, возможно, есть функция Windows, которая может конвертировать целые строки, например C++ boost::to_upper_copy<std::string>
(если это еще не медленнее, см. ниже).
Также обратите внимание, чтообтекание строк ASCII может быть довольно эффективно векторизовано SIMD. Я написал функцию case-flip для одного вектора в другом ответе, используя встроенные функции C SSE; он может быть адаптирован к upcase вместо flipcase. Это должно привести к значительному ускорению, если вы тратите много времени на преобразование строк, длина которых превышает 16 байтов, и которые, как вы знаете, являются ASCII.
На самом деле, tost_upper_copy() Boost, по- видимому, компилируется в чрезвычайно медленный код, примерно в 10 раз медленнее, чемtoupper
, Смотрите эту ссылку для моего векторизации strtoupper(dst,src)
, который только для ASCII, но может быть расширен с резервным вариантом при обнаружении байтов src не-ASCII.
Как ваш текущий код обрабатывает UTF-8? Поддерживать локали, отличные от ASCII, не очень полезно, если вы предполагаете, что все символы являются одним байтом. IIRC, Windows использует UTF-16 для большинства вещей, что вызывает сожаление, потому что оказалось, что миру нужно более 2^16 кодовых точек. UTF-16 - это кодировка Unicode переменной длины, аналогичная UTF-8, но без преимущества чтения ASCII. Фиксированная ширина имеет много преимуществ, но, к сожалению, вы не можете предполагать это даже с UTF-16. Ява тоже допустила эту ошибку и застряла с UTF-16.
#define __ctype_toupper \
((int32_t *) _NL_CURRENT (LC_CTYPE, _NL_CTYPE_TOUPPER) + 128)
int toupper (int c) {
return c >= -128 && c < 256 ? __ctype_toupper[c] : c;
}
Asm от Ubuntu 15.10 x86-64 /lib/x86_64-linux-gnu/libc.so.6
является:
## disassembly from objconv -fyasm -v2 /lib/x86_64-linux-gnu/libc.so.6 /dev/stdout 2>&1
toupper:
lea edx, [rdi+80H] ; 0002E300 _ 8D. 97, 00000080
movsxd rax, edi ; 0002E306 _ 48: 63. C7
cmp edx, 383 ; 0002E309 _ 81. FA, 0000017F
ja ?_01766 ; 0002E30F _ 77, 19
mov rdx, qword [rel ?_37923] ; 0002E311 _ 48: 8B. 15, 00395AA8(rel)
sub rax, -128 ; 0002E318 _ 48: 83. E8, 80
mov rdx, qword [fs:rdx] ; 0002E31C _ 64 48: 8B. 12
mov rdx, qword [rdx] ; 0002E320 _ 48: 8B. 12
mov rdx, qword [rdx+48H] ; 0002E323 _ 48: 8B. 52, 48
mov eax, dword [rdx+rax*4] ; 0002E327 _ 8B. 04 82 ## the final table lookup, indexing an array of 4B ints
?_01766:
rep ret ; actual objconv output shows the prefix on a separate line
Таким образом, требуется заблаговременно, если аргумент arg не находится в диапазоне 0–0xFF (поэтому эта ветвь должна предсказать совершенно не взятым), в противном случае он находит таблицу для текущей локали, которая включает три разыменования указателя: одна загрузка из глобальный, и один локальный поток, и еще одна разыменование. Затем он фактически индексируется в таблицу с 256 записями.
Это вся функция библиотеки; toupper
Метка в разборке - это то, что вызывает ваш код. (Ну, через слой косвенного обращения через PLT из-за динамического связывания, но после первого вызова вызывает ленивый поиск символа, это всего лишь один дополнительный jmp
инструкция между вашим кодом и этими 11 insns в библиотеке.)