У нас тут идет второй день тренинг по С++ и unit-тестированию. Ведет Kevlin Henney. Отличный дядка.
Все как обычно -- стараемся приучать к культуре разработки через тесты, ну и попутно склонить патриотов С к С++, убедив их, что на С++ можно таки писать также эффективно, как и на С. Да еще и в разы быстрее.
Зашла тема про std::fill(). Я вставил словечко, что мол fill() работает также быстро как и memset(), так как он используется в fill() для простых типов.
Написали программу, в которой есть интересный момент.
#include <cstdlib>
#include <algorithm>
int main(int argc, char* argv[]) {
int mode = argc > 1 ? std::atoi(argv[1]) : 1;
int n = 1024 * 1024 * 1024 * 1;
char* buf = new char[n];
if (mode == 1)
std::memset(buf, 0, n * sizeof(*buf));
else if (mode == 2)
bzero(buf, n * sizeof(*buf));
else if (mode == 3)
std::fill(buf, buf + n, 0);
else if (mode == 4)
std::fill(buf, buf + n, '\0');
return buf[0];
}
Обратите внимание на ветки 3 и 4. Они почти одно и то же, но не совсем.
В целом была мысль получить вот эту специализацию fill():
// Specialization: for one-byte types we can use memset.
inline void
fill(unsigned char* __first, unsigned char* __last, const unsigned char& __c)
{
__glibcxx_requires_valid_range(__first, __last);
const unsigned char __tmp = __c;
std::memset(__first, __tmp, __last - __first);
}
Итак, Makefile:
all: build run
.SILENT:
target = memset_bzero_fill
build:
g++ -O3 -o $(target) $(target).cpp
run: run-memset run-bzero run-fill-1 run-fill-2
go:
(time -p ./$(target) $(mode)) 2>&1 | head -1 | cut -d' ' -f 2
run-memset:
echo $@ `$(MAKE) go mode=1`
run-bzero:
echo $@ `$(MAKE) go mode=2`
run-fill-1:
echo $@ `$(MAKE) go mode=3`
run-fill-2:
echo $@ `$(MAKE) go mode=4`
Компилятор "gcc version 4.2.1 (Apple Inc. build 5666) (dot 3)".
Запускаем:
run-memset 1.47
run-bzero 1.45
run-fill-1 1.69
run-fill-2 1.42
Видно, как ветка 3 (run-fill-1) значительно тормозит, по сравнению с 4, хотя разница всего в типе последнего параметра - 0 и '\0'.
Смотрим ассемблер:
(gdb) disass main
Dump of assembler code for function main:
0x0000000100000e70 <main+0>: push %rbp
0x0000000100000e71 <main+1>: mov %rsp,%rbp
0x0000000100000e74 <main+4>: push %r12
0x0000000100000e76 <main+6>: push %rbx
0x0000000100000e77 <main+7>: dec %edi
0x0000000100000e79 <main+9>: jle 0x100000ec3 <main+83>
0x0000000100000e7b <main+11>: mov 0x8(%rsi),%rdi
0x0000000100000e7f <main+15>: callq 0x100000efe <dyld_stub_atoi>
0x0000000100000e84 <main+20>: mov %eax,%r12d
0x0000000100000e87 <main+23>: mov $0x40000000,%edi
0x0000000100000e8c <main+28>: callq 0x100000ef8 <dyld_stub__Znam>
0x0000000100000e91 <main+33>: mov %rax,%rbx
0x0000000100000e94 <main+36>: cmp $0x1,%r12d
0x0000000100000e98 <main+40>: je 0x100000eac <main+60> ; mode == 1
0x0000000100000e9a <main+42>: cmp $0x2,%r12d
0x0000000100000e9e <main+46>: je 0x100000eac <main+60> ; mode == 2
0x0000000100000ea0 <main+48>: cmp $0x3,%r12d
0x0000000100000ea4 <main+52>: je 0x100000ed2 <main+98> ; mode == 3
0x0000000100000ea6 <main+54>: cmp $0x4,%r12d
0x0000000100000eaa <main+58>: jne 0x100000ebb <main+75> ; mode != 4 -> выход
; Реалиазация через memset().
0x0000000100000eac <main+60>: mov $0x40000000,%edx ; mode = 1, 2 или 4
0x0000000100000eb1 <main+65>: xor %esi,%esi
0x0000000100000eb3 <main+67>: mov %rbx,%rdi
0x0000000100000eb6 <main+70>: callq 0x100000f0a <dyld_stub_memset>
0x0000000100000ebb <main+75>: movsbl (%rbx),%eax ; выход
0x0000000100000ebe <main+78>: pop %rbx
0x0000000100000ebf <main+79>: pop %r12
0x0000000100000ec1 <main+81>: leaveq
0x0000000100000ec2 <main+82>: retq
0x0000000100000ec3 <main+83>: mov $0x40000000,%edi
0x0000000100000ec8 <main+88>: callq 0x100000ef8 <dyld_stub__Znam>
0x0000000100000ecd <main+93>: mov %rax,%rbx
0x0000000100000ed0 <main+96>: jmp 0x100000eac <main+60>
; Реализация на обычных командах.
0x0000000100000ed2 <main+98>: movb $0x0,(%rax) ; mode = 3
0x0000000100000ed5 <main+101>: mov $0x1,%eax
0x0000000100000eda <main+106>: nopw 0x0(%rax,%rax,1)
0x0000000100000ee0 <main+112>: movb $0x0,(%rax,%rbx,1)
0x0000000100000ee4 <main+116>: inc %rax
0x0000000100000ee7 <main+119>: cmp $0x40000000,%rax
0x0000000100000eed <main+125>: jne 0x100000ee0 <main+112>
0x0000000100000eef <main+127>: movsbl (%rbx),%eax ; выход
0x0000000100000ef2 <main+130>: pop %rbx
0x0000000100000ef3 <main+131>: pop %r12
0x0000000100000ef5 <main+133>: leaveq
0x0000000100000ef6 <main+134>: retq
Видно, что благодаря оптимизации, ветки 1, 2 и 4 реализованы одинаково - через memset(). Вызов fill() в ветке 4 удалось свести к memset().
Но вот ветка 3 реализована в виде ручного цикла. Компилятор, конечно, неплохо поработал -- цикл практически идеальный, но это все равно работает медленнее, чем хитрый memset(), который использует всякие ухищрения групповых ассемблерных операций.
Неверный тип нуля не дал компилятору правильно выбрать специализацию шаблона.
Мораль? И мораль тут не очень хорошая.
Мне кажется, что количество людей, которые напишут "std::fill(buf, buf + n, 0)", разительно больше, чем "std::fill(buf, buf + n, '\0')".
А разница весьма существенна.
Посты по теме: