Подсев на диету “медленных углеводов” (Slow-Carb), я завел блог Диета и спорт для ленивых, который веду преимущественно с айфона (О Tumblr! Ты лучший!). Активно “массирую” фотографиями Инстаграм и в частности едой, которую готовлю.
Но в апреле я таки съездил на ежегодную конференцию ACCU 2013.
В этом году она проходила в Бристоле и была на 99% посвящена новому стандарту C++ 2011. Кроме этого одним из генеральных спонсоров была компания Блумберг, поэтому было непривычное количество друзей и знакомых.
Гвоздем программы был сам Страуструп, лично.
Мне до Бристоля два часа на машине, и это было удобно. А тот момент я уже “сидел” на диете Slow-Carb, поэтому поездка не обошлась без ее элементов. Про как и почему, про гантелю в багажнике и kettlebell swing я писал в кулинарном блоге в серии постов, начиная с Эксперимент по готовке еды в командировке.
Ну все, хватит про кулинарию, и ближе к программированию и С++.
Все презентации были, как я уже сказал, про C++ 2011. Многие темы уже оскомину набили, но для освежения памяти полезно. Конечно, выступления мастеров типа Бьярна Страуструпа, Джона Лакоса, Энтони Уильямса или Кевлина Хенни всегда классные.
Не могу не похвастаться парочкой фотографий, что удалось сделать.
Мастер
Джон Лакос
А в качестве развлекательного бонуса фотографии моего пятиминутного экспромта в рамках Lighting Talks, где я рассказываю про свою коллекцию микропроцессоров Intel 8080 и про эмулятор Радио-86РК.
Увы, в этом году мне пришлось самому оплатить конференцию вместе проживанием, но я ни разу не жалею о таком варианте отпуска, ибо было очень интересно и полезно.
На сайте ACCU доступны практические все материалы конференции.
]]>putenv
.
Я выкатил этот класс в наш QA, который работает на множестве платформ
(AIX, HP-UX, Solaris, Linux, Windows). И вроде все шло нормально, unit-тесты
проходили, основной код не падал. Увы, но QA машины, управляемые
Hudson/Jenkins обычно дико перегружены, и обычно на них начинают вылезать
самые неожиданные косяки. Через неделю работы стало видно, что на AIX
иногда происходят странные падения при вызове std::system()
. Причем
что-то странное происходило именно внутри этой функции. Но так как чудес не
бывает, то необъяснимые вещи обычно являются следствиями проблем с памятью.
truss, вставленный в командную строку, передавамую в system()
, показал,
что иногда блок переменных окружения дочернего процесса имеет разрушенные
значения у некоторых переменных.
Начали копать в моем новом классе. Вот его упрощенная версия, но которая
содержит коварный баг. Желающие могут сначала подумать, в чем тут может быть
проблема, а потом читать дальше. Простой main()
внизу позволяет багу
проявиться.
#include <vector> #include <map> #include <string> #include <unistd.h> class EnvironmentVariablesManager { public: typedef std::vector<char> VariableContainer; typedef std::map<std::string, VariableContainer> Variables; void put(const std::string& name, const std::string& value) { VariableContainer pair; PairToContainer(name, value, &pair); const std::pair<Variables::iterator, bool> inserted = vars_.insert(std::make_pair(name, pair)); if (!inserted.second) inserted.first->second = pair; putenv(&inserted.first->second[0]); } private: void PairToContainer(const std::string& name, const std::string& value, VariableContainer* pair) const { pair->clear(); std::copy(name.begin(), name.end(), std::back_inserter(*pair)); pair->push_back('='); std::copy(value.begin(), value.end(), std::back_inserter(*pair)); pair->push_back('\0'); } Variables vars_; }; int main() { EnvironmentVariablesManager env; env.put("DB2_HOME", "a"); env.put("DB2_HOME", "12345678"); }
Если этот код запустить под valgrind на Линуксе (я тестировал у себя
на OSX), вылезает странное сообщение об использовании памяти внутри putenv
после ее освобождения.
clang++ -o putenv_check putenv_test.cpp && valgrind ./putenv_check
==1046== Memcheck, a memory error detector
==1046== Copyright (C) 2002-2012, and GNU GPL'd, by Julian Seward et al.
==1046== Using Valgrind-3.8.1 and LibVEX; rerun with -h for copyright info
==1046== Command: ./putenv_check
==1046==
--1046-- ./putenv_check:
--1046-- dSYM directory is missing; consider using --dsymutil=yes
==1046== Invalid read of size 1
==1046== at 0x2A8A3B: __findenv (in /usr/lib/system/libsystem_c.dylib)
==1046== by 0x232C62: __setenv (in /usr/lib/system/libsystem_c.dylib)
==1046== by 0x216A7E: putenv (in /usr/lib/system/libsystem_c.dylib)
==1046== by 0x100001999: EnvironmentVariablesManager::put(std::string const&, std::string const&) (in ./putenv_check)
==1046== by 0x1000015DE: main (in ./putenv_check)
==1046== Address 0x100012560 is 0 bytes inside a block of size 11 free'd
==1046== at 0x563A: free (in /usr/local/Cellar/valgrind/3.8.1/lib/valgrind/vgpreload_memcheck-amd64-darwin.so)
==1046== by 0x10000208C: __gnu_cxx::new_allocator<char>::deallocate(char*, unsigned long) (in ./putenv_check)
==1046== by 0x10000201D: std::_Vector_base<char, std::allocator<char> >::_M_deallocate(char*, unsigned long) (in ./putenv_check)
==1046== by 0x100002483: std::vector<char, std::allocator<char> >::operator=(std::vector<char, std::allocator<char> > const&) (in ./putenv_check)
==1046== by 0x1000018A8: EnvironmentVariablesManager::put(std::string const&, std::string const&) (in ./putenv_check)
==1046== by 0x1000015DE: main (in ./putenv_check)
==1046==
==1046==
==1046== HEAP SUMMARY:
==1046== in use at exit: 2,425 bytes in 34 blocks
==1046== total heap usage: 58 allocs, 24 frees, 2,824 bytes allocated
==1046==
==1046== LEAK SUMMARY:
==1046== definitely lost: 18 bytes in 1 blocks
==1046== indirectly lost: 0 bytes in 0 blocks
==1046== possibly lost: 0 bytes in 0 blocks
==1046== still reachable: 2,407 bytes in 33 blocks
==1046== suppressed: 0 bytes in 0 blocks
==1046== Rerun with --leak-check=full to see details of leaked memory
==1046==
==1046== For counts of detected and suppressed errors, rerun with: -v
==1046== ERROR SUMMARY: 9 errors from 1 contexts (suppressed: 1 from 1)
Сходу, по крайне мере мне, не совсем очевидно, почему это происходит. Причем,
если убрать второй вызов env.put("DB2_HOME", "12345678")
, то ошибка
изчезает. Поэтому подозрение пало на строки:
if (!inserted.second) inserted.first->second = pair; putenv(&inserted.first->second[0]);
Если изменить немного код (фактически, мы тоже копируем массив, но иным способом):
if (!inserted.second) inserted.first->second.assign(pair.begin(), pair.end());
то сообщение об ошибке меняется:
--1087-- ./putenv_check:
--1087-- dSYM directory is missing; consider using --dsymutil=yes
==1087== Invalid read of size 1
==1087== at 0x2A8A3B: __findenv (in /usr/lib/system/libsystem_c.dylib)
==1087== by 0x232C62: __setenv (in /usr/lib/system/libsystem_c.dylib)
==1087== by 0x216A7E: putenv (in /usr/lib/system/libsystem_c.dylib)
==1087== by 0x1000016A9: EnvironmentVariablesManager::put(std::string const&, std::string const&) (in ./putenv_check)
==1087== by 0x10000129E: main (in ./putenv_check)
==1087== Address 0x100013560 is 0 bytes inside a block of size 11 free'd
==1087== at 0x563A: free (in /usr/local/Cellar/valgrind/3.8.1/lib/valgrind/vgpreload_memcheck-amd64-darwin.so)
==1087== by 0x100001D9C: __gnu_cxx::new_allocator<char>::deallocate(char*, unsigned long) (in ./putenv_check)
==1087== by 0x100001D2D: std::_Vector_base<char, std::allocator<char> >::_M_deallocate(char*, unsigned long) (in ./putenv_check)
==1087== by 0x1000022E9: void std::vector<char, std::allocator<char> >::_M_assign_aux<__gnu_cxx::__normal_iterator<char*, std::vector<char, std::allocator<char> > > >(__gnu_cxx::__normal_iterator<char*, std::vector<char, std::allocator<char> > >, __gnu_cxx::__normal_iterator<char*, std::vector<char, std::allocator<char> > >, std::forward_iterator_tag) (in ./putenv_check)
==1087== by 0x1000021C4: void std::vector<char, std::allocator<char> >::_M_assign_dispatch<__gnu_cxx::__normal_iterator<char*, std::vector<char, std::allocator<char> > > >(__gnu_cxx::__normal_iterator<char*, std::vector<char, std::allocator<char> > >, __gnu_cxx::__normal_iterator<char*, std::vector<char, std::allocator<char> > >, std::__false_type) (in ./putenv_check)
==1087== by 0x1000020A4: void std::vector<char, std::allocator<char> >::assign<__gnu_cxx::__normal_iterator<char*, std::vector<char, std::allocator<char> > > >(__gnu_cxx::__normal_iterator<char*, std::vector<char, std::allocator<char> > >, __gnu_cxx::__normal_iterator<char*, std::vector<char, std::allocator<char> > >) (in ./putenv_check)
==1087== by 0x1000015BF: EnvironmentVariablesManager::put(std::string const&, std::string const&) (in ./putenv_check)
==1087== by 0x10000129E: main (in ./putenv_check)
Теперь более менее ясно, что происходит. Но для полного понимания приведу еще более простой код на С:
#include <unistd.h> #include <stdlib.h> int main() { char *v1, *v2; v1 = malloc(10); strcpy(v1, "x=123"); putenv(v1); free(v1); v2 = malloc(10); strcpy(v2, "x=123"); putenv(v2); free(v2); return 0; }
Данный код, запущенный под valgrind, выдает следующее (этот trace уже с Линукса):
==523== Memcheck, a memory error detector
==523== Copyright (C) 2002-2010, and GNU GPL'd, by Julian Seward et al.
==523== Using Valgrind-3.6.0 and LibVEX; rerun with -h for copyright info
==523== Command: ./putenv_test
==523==
==523== Invalid read of size 1
==523== at 0x4A07CF9: __GI_strncmp (mc_replace_strmem.c:400)
==523== by 0x3E1C235649: __add_to_environ (in /lib64/libc-2.12.so)
==523== by 0x3E1C2353CD: putenv (in /lib64/libc-2.12.so)
==523== by 0x4A0952D: putenv (mc_replace_strmem.c:1165)
==523== by 0x400607: main (in /storage2/home3/ademin/test/env/t)
==523== Address 0x4c28040 is 0 bytes inside a block of size 10 free'd
==523== at 0x4A0595D: free (vg_replace_malloc.c:366)
==523== by 0x4005D7: main (in /storage2/home3/ademin/test/env/t)
==523==
==523== Invalid read of size 1
==523== at 0x4A07D14: __GI_strncmp (mc_replace_strmem.c:400)
==523== by 0x3E1C235649: __add_to_environ (in /lib64/libc-2.12.so)
==523== by 0x3E1C2353CD: putenv (in /lib64/libc-2.12.so)
==523== by 0x4A0952D: putenv (mc_replace_strmem.c:1165)
==523== by 0x400607: main (in /storage2/home3/ademin/test/env/t)
==523== Address 0x4c28040 is 0 bytes inside a block of size 10 free'd
==523== at 0x4A0595D: free (vg_replace_malloc.c:366)
==523== by 0x4005D7: main (in /storage2/home3/ademin/test/env/t)
==523==
==523== Invalid read of size 1
==523== at 0x3E1C235652: __add_to_environ (in /lib64/libc-2.12.so)
==523== by 0x3E1C2353CD: putenv (in /lib64/libc-2.12.so)
==523== by 0x4A0952D: putenv (mc_replace_strmem.c:1165)
==523== by 0x400607: main (in /storage2/home3/ademin/test/env/t)
==523== Address 0x4c28041 is 1 bytes inside a block of size 10 free'd
==523== at 0x4A0595D: free (vg_replace_malloc.c:366)
==523== by 0x4005D7: main (in /storage2/home3/ademin/test/env/t)
==523==
==523==
==523== HEAP SUMMARY:
==523== in use at exit: 0 bytes in 0 blocks
==523== total heap usage: 3 allocs, 3 frees, 492 bytes allocated
==523==
==523== All heap blocks were freed -- no leaks are possible
==523==
==523== For counts of detected and suppressed errors, rerun with: -v
==523== ERROR SUMMARY: 3 errors from 3 contexts (suppressed: 6 from 6)
Но если убрать первый free(v1);
или передвинуть его после второго
putenv
, то valgrind ничего не находит.
Итак, вывод: функция putenv
требует, чтобы в момент ее вызова память, в
которой хранится передыдущее значение переменной, обязана быть доступной и
все еще содержать правильное старое значение.
Каким-то образом, putenv
пытает читать старое значение при установке нового.
Теперь назад к C++. Если оригинальный код:
if (!inserted.second) inserted.first->second = pair;
заменить на следущий:
if (!inserted.second) inserted.first->second.swap(pair);
то проблема изчезает (valgrind более ничего не находит подозрительного).
В чем разница? А в том, что первый код в процессе копирования разрушает (освобождает, перемещает) старое значение. Поэтому последующий вызов putenv’а будет пытаться обратиться к уже освобожденной памяти.
Второй же код, делая swap
, перекидывает владение данными старого значения
из хранилища (map) во временную переменную pair
, а данные из pair
отдает
хранилищу. А так как переменная pair
“изчезнет” только в конце функции, то на
момент вызова putenv
старое значение будет все еще доступно. Фактически, мы
откладываем момент уничтожения старого значения.
Вот такая вот история. Если честно, это один из крутейших багов с памятью, что я лично встречал последнее время.
]]>snprintf()
является “правильным” способом форматного
преобразования в С, так как есть возможность контролировать длину рождаемых
данных. Но как у остальных функций подобного рода, типа strcpy()
, у нее есть
мутный момент в плане нуля на конце, если буфер кончился раньше времени.
Мне хотелость определенности в этом вопросе, поэтому программа:
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#ifdef WINDOWS
#define snprintf _snprintf
#endif
void test(const int capacity) {
char buf[1024];
int n;
strcpy(buf, "abcdefghijk");
n = snprintf(buf, capacity, "%d", 123);
printf("capacity=%d, n=%d, buf=[%s] (length %d)\n",
capacity, n, buf, (int)strlen(buf));
}
int main() {
test(0);
test(1);
test(2);
test(3);
test(4);
test(5);
return 0;
}
Данный код проверяет, как именно snprintf()
использует предоставленный
буфер, если результат полностью не вмещается, и добавляется ли ноль в конце.
Запускать будем на разных системах и компиляторах.
capacity=0, n=3, buf=[abcdefghijk] (length 11)
capacity=1, n=3, buf=[] (length 0)
capacity=2, n=3, buf=[1] (length 1)
capacity=3, n=3, buf=[12] (length 2)
capacity=4, n=3, buf=[123] (length 3)
capacity=5, n=3, buf=[123] (length 3)
capacity=0, n=3, buf=[abcdefghijk] (length 11)
capacity=1, n=3, buf=[] (length 0)
capacity=2, n=3, buf=[1] (length 1)
capacity=3, n=3, buf=[12] (length 2)
capacity=4, n=3, buf=[123] (length 3)
capacity=5, n=3, buf=[123] (length 3)
capacity=0, n=3, buf=[abcdefghijk] (length 11)
capacity=1, n=3, buf=[] (length 0)
capacity=2, n=3, buf=[1] (length 1)
capacity=3, n=3, buf=[12] (length 2)
capacity=4, n=3, buf=[123] (length 3)
capacity=5, n=3, buf=[123] (length 3)
capacity=0, n=3, buf=[abcdefghijk] (length 11)
capacity=1, n=3, buf=[] (length 0)
capacity=2, n=3, buf=[1] (length 1)
capacity=3, n=3, buf=[12] (length 2)
capacity=4, n=3, buf=[123] (length 3)
capacity=5, n=3, buf=[123] (length 3)
capacity=0, n=3, buf=[abcdefghijk] (length 11)
capacity=1, n=3, buf=[] (length 0)
capacity=2, n=3, buf=[1] (length 1)
capacity=3, n=3, buf=[12] (length 2)
capacity=4, n=3, buf=[123] (length 3)
capacity=5, n=3, buf=[123] (length 3)
capacity=0, n=3, buf=[abcdefghijk] (length 11)
capacity=1, n=3, buf=[] (length 0)
capacity=2, n=3, buf=[1] (length 1)
capacity=3, n=3, buf=[12] (length 2)
capacity=4, n=3, buf=[123] (length 3)
capacity=5, n=3, buf=[123] (length 3)
capacity=0, n=0, buf=[abcdefghijk] (length 11)
capacity=1, n=-1, buf=[] (length 0)
capacity=2, n=-1, buf=[1] (length 1)
capacity=3, n=-1, buf=[12] (length 2)
capacity=4, n=3, buf=[123] (length 3)
capacity=5, n=3, buf=[123] (length 3)
capacity=0, n=3, buf=[abcdefghijk] (length 11)
capacity=1, n=3, buf=[] (length 0)
capacity=2, n=3, buf=[1] (length 1)
capacity=3, n=3, buf=[12] (length 2)
capacity=4, n=3, buf=[123] (length 3)
capacity=5, n=3, buf=[123] (length 3)
capacity=0, n=3, buf=[abcdefghijk] (length 11)
capacity=1, n=3, buf=[] (length 0)
capacity=2, n=3, buf=[1] (length 1)
capacity=3, n=3, buf=[12] (length 2)
capacity=4, n=3, buf=[123] (length 3)
capacity=5, n=3, buf=[123] (length 3)
capacity=0, n=3, buf=[abcdefghijk] (length 11)
capacity=1, n=3, buf=[] (length 0)
capacity=2, n=3, buf=[1] (length 1)
capacity=3, n=3, buf=[12] (length 2)
capacity=4, n=3, buf=[123] (length 3)
capacity=5, n=3, buf=[123] (length 3)
capacity=0, n=3, buf=[abcdefghijk] (length 11)
capacity=1, n=3, buf=[] (length 0)
capacity=2, n=3, buf=[1] (length 1)
capacity=3, n=3, buf=[12] (length 2)
capacity=4, n=3, buf=[123] (length 3)
capacity=5, n=3, buf=[123] (length 3)
capacity=0, n=-1, buf=[abcdefghijk] (length 11)
capacity=1, n=-1, buf=[1bcdefghijk] (length 11)
capacity=2, n=-1, buf=[12cdefghijk] (length 11)
capacity=3, n=3, buf=[123defghijk] (length 11)
capacity=4, n=3, buf=[123] (length 3)
capacity=5, n=3, buf=[123] (length 3)
capacity=0, n=-1, buf=[abcdefghijk] (length 11)
capacity=1, n=-1, buf=[1bcdefghijk] (length 11)
capacity=2, n=-1, buf=[12cdefghijk] (length 11)
capacity=3, n=3, buf=[123defghijk] (length 11)
capacity=4, n=3, buf=[123] (length 3)
capacity=5, n=3, buf=[123] (length 3)
capacity=0, n=3, buf=[abcdefghijk] (length 11)
capacity=1, n=3, buf=[] (length 0)
capacity=2, n=3, buf=[1] (length 1)
capacity=3, n=3, buf=[12] (length 2)
capacity=4, n=3, buf=[123] (length 3)
capacity=5, n=3, buf=[123] (length 3)
capacity=0, n=3, buf=[abcdefghijk] (length 11)
capacity=1, n=3, buf=[] (length 0)
capacity=2, n=3, buf=[1] (length 1)
capacity=3, n=3, buf=[12] (length 2)
capacity=4, n=3, buf=[123] (length 3)
capacity=5, n=3, buf=[123] (length 3)
На всех UNIX’ах (SunOS, Linux, AIX, OSX), кроме HP-UX, буфер не
трогается, если его длина 0, завершающий \0
учитывается в длине рождаемых
данных (то есть, если буфер длиной 1, то туда влезет только один символ конца
строки), и фукнция всегда возващает длину данных (без учета нуля в конце),
которые могли бы быть записаны, если б хватило буфера. Это число можно
использовать для выделения буфера достаточной длины при повторном вызове
функции.
Увы, на HP-UX, если буфер маловат, то возвращается -1. В этом случае не понятно, как определить длину требуемого буфера. Методом дихотомии? Странно.
В Windows ситуация еще хуже. Мало того, что функция не возвращает длину
требуемого буфера, так еще не учитывает ноль в расчете его длины. То есть
когда размера буфера не достаточно, то ноль в конце не добавляется.
Но Microsoft говорит, что не стоит использоваться snprintf()
вообще,
а переключиться на _snrpintf_s()
.
Теперь понятно, почему интернет полон темами типа “a portable snprintf implementation”.
Для моей конкретной задачи неплохо подошла бы функция asprintf, так как меня не пугает malloc на каждом вызове, но увы, функция нестандартная, и то же HP-UX ее не имеет.
]]>Итак, функция, которая что-то делает с файлом:
std::string DoSomethingWithFile(const std::string& name) { std::ifstream is(name); // ... return a_value; }
Что плохого в этой функции? Для ее тестирования нужен реальный файл на файловой системе. В принципе, это не смертельно для unit-тестов, но как-то коряво, особенно если логика требует большого количества тестов.
Лично я взял себе за правило всегда разделять работу с файлом и его открытие:
std::string DoSomethingWithStream(std::istream* is) { // ... return a_value; } std::string DoSomethingWithFile(const std::string& name) { std::ifstream is(name); return DoSomethingWithStream(&is); }
Тогда первую функцию можно в хвост и в гриву оттестировать, подсунув ей std::istringstream в тесте. А вторую, прикрыв глаза рукой, можно не тестировать или тестировать примитивно на одном реальном файле просто на предмет того, что она может его открыть.
Еще одно мое собственное правило: в принципе стараться не использовать
стандартные потоки std::cin/cout/cerr напрямую, а всегда передавать их как
параметр. Например, есть фукнция usage()
, которая выводит справку о
программе. Если cout/cerr
передать через параметр, то можно будет
делать тесты на наличие определенных строк в выводе этой функции. Добавил
новый функционал и наряду с прочими тестами добавил тест для проверки, что
usage()
что-то там выводит про этот новый функционал.
И ничего, что нет (и не будет) на клавиатуре, кнопок BACKSPACE и INSERT (я давно знаю, что жизнь без них все так же прекрасна) и красная кнопочка в углу окна полностью НЕ закрывает программу, а надо жать CMD-Q (я помню клавиатурных комбинаций больше, чем английских слов). А все эти “проблемы” только на пользу – меньше людей будут пользоваться вашими продуктами, что значит: мне больше достанется.
СПАСИБО!
]]>Имеем:
1234\x04\x1fooo\xff
Печататься должно:
1234<04><1f>ooo<ff>
Рождается функция:
// This function converts all non-ASCII characters (with codes less than 32 // and greater than 126) into the <xx> form where "xx" is a hexadecimal code // of the character. void MakeAllPrintable(const char* from, char* to, int to_capacity) { const static char hex[] = "0123456789abcdef"; for (; *from && to_capacity > 1; ++from) { const unsigned char c = *from; if (c < 32 || c > 126) { if (to_capacity < 4 + 1) break; *to++ = '<'; *to++ = hex[(c >> 4) & 0xf]; *to++ = hex[c & 0xf]; *to++ = '>'; } else { *to++ = c; } } *to = '\0'; }
Увы, это С, а наша системы сборки частей на С, хоть и имеет cmockery, но добавление тестов всегда связано с ручным прописываением имен фунцкий, а это будет означать пересборку еще и makefile’ов, а это будет означать проверку этой пересборки на всех типах ОС, и в этоге на подготовку уйдет час или больше. А если потихоньку просто добавить функцию без теста, то 5 минут, так как изменится только один исходник. Мои мысли: “Ну я, типа, такой катаный колобок, врядли налажаю в столь простой функции. Так что можно без тестов.“. И, конечно, налажал. Код выше имеет конкретный косяк. Он бы, естественно, всплыл, может даже на code review, но в итоге я отогнал все ренегатские мысли, сделал всю рутину и сел за тесты, хотя по уму, я их должен быть писать до.
Очевидно, что вполне логичный (предпоследний) тест, там, где последний параметр 5, сразу показал, что я забыл проверку максимальной длины буфера (эдакая назадача!):
void test_MakeAllPrintable(void **state) { char buf[16 * 1024]; MakeAllPrintable("", buf, sizeof(buf)); assert_int_equal(strlen(buf), 0); MakeAllPrintable("abc", buf, sizeof(buf)); assert_string_equal(buf, "abc"); // This check verifies that the function always cleans the buffer at start. MakeAllPrintable("", buf, sizeof(buf)); assert_int_equal(strlen(buf), 0); // This check verifies that the function doesn't overwrite the given buffer // and always reserves the last byte for '\0'. MakeAllPrintable("12345678", buf, 5); assert_string_equal(buf, "1234");$ MakeAllPrintable("1\x01\r\n\t\x1f 2\x7e\x7f\x80\xff", buf, sizeof(buf)); assert_string_equal(buf, "1<01><0d><0a><09><1f> 2~<7f><80><ff>"); }
А вот как код должен был выглядеть:
// This function converts all non-ASCII characters (with codes less than 32 // and greater than 126) into the <xx> form where "xx" is a hexadecimal code // of the character. void MakeAllPrintable(const char* from, char* to, int to_capacity) { const static char hex[] = "0123456789abcdef"; for (; *from && to_capacity > 1; ++from) { const unsigned char c = *from; if (c < 32 || c > 126) { if (to_capacity < 4 + 1) break; *to++ = '<'; *to++ = hex[(c >> 4) & 0xf]; *to++ = hex[c & 0xf]; *to++ = '>'; to_capacity -= 4; // (!!!) } else { *to++ = c; --to_capacity; // (!!!) } } *to = '\0'; }
Пример, безусловно, примитивный, но очень показательный. Принцип простой: “нет тестов, нет кода”. Когда жизненный цикл проекта более нескольких дней, тесты обязаны быть частью кода, и по мне, не должно быть разделения на production code и test code по большому счету, так как первое без второго не существует.
]]>У функции putenv()
, которая создает (или удаляет) переменную окружения
есть одно неприятное свойство: она не создает копию аргумента, а использует
его напрямую по указателю. Более того, ее аргумент является неконстантным
указателем. Тут и автоматические объекты нельзя использовать, да и даже
строковую константу надо передавать, снимая const
, что и не очень правильно.
Тут и начинаются всякие несуразности типа malloc
или strdup
.
Итак, класс EnvironmentVariablesManager
, который является небольшой
надстройкой над putenv()
и getenv()
. Для putenv()
класс сохраняет
копию переменной (пары “имя=значение”) во внутреннем хранилище.
Класс разрабатывался как универсальный под ряд платформ: Linux, AIX, HP-UX, Solaris и Windows.
Файл EnvironmentVariablesManager.h:
#ifndef ENVIRONMENT_VARIABLE_MANAGER_H #define ENVIRONMENT_VARIABLE_MANAGER_H #include <string> #include <vector> #include <map> class EnvironmentVariablesManager { public: typedef std::vector<char> VariableContainer; typedef std::map<std::string, VariableContainer> Variables; EnvironmentVariablesManager() {} void put(const std::string& name, const std::string& value); std::string get(const std::string& name) const; void del(const std::string& name); static void PutOSVariable(char* value); static std::string GetOSVariable(const char* name); static bool IsOSVariableSet(const char* name); private: VariableContainer PairToContainer(const std::string& name, const std::string& value) const; Variables vars_; // This class is not copiable. EnvironmentVariablesManager(const EnvironmentVariablesManager&); void operator=(const EnvironmentVariablesManager&); }; #endif
Файл EnvironmentVariablesManager.cpp:
#include "EnvironmentVariablesManager.h" #ifdef WINDOWS #include <windows.h> #else #include <unistd.h> #endif #include <vector> #include <map> #include <string> #include <iterator> #include <algorithm> #include <cassert> void EnvironmentVariablesManager::put(const std::string& name, const std::string& value) { const VariableContainer pair = PairToContainer(name, value); const std::pair<Variables::iterator, bool> inserted = vars_.insert(std::make_pair(name, pair)); if (!inserted.second) inserted.first->second = pair; char* data = &(inserted.first->second[0]); PutOSVariable(data); } std::string EnvironmentVariablesManager::get(const std::string& name) const { return GetOSVariable(name.c_str()); } void EnvironmentVariablesManager::del(const std::string& name) { put(name, ""); } void EnvironmentVariablesManager::PutOSVariable(char* value) { ::putenv(value); } std::string EnvironmentVariablesManager::GetOSVariable(const char* name) { #ifdef WINDOWS size_t sz = 0; assert(getenv_s(&sz, NULL, 0, name) == 0); if (sz == 0) return std::string(); std::vector<char> value(sz + 1); assert(getenv_s(&sz, &value[0], sz, name) == 0); return std::string(&value[0], sz - 1); #else const char* const value = std::getenv(name); return value ? value : ""; #endif } bool EnvironmentVariablesManager::IsOSVariableSet(const char* name) { #ifdef WINDOWS size_t sz = 0; assert(getenv_s(&sz, NULL, 0, name) == 0); return sz > 0; #else const char* value = std::getenv(name); return value != NULL && *value != '\0'; #endif } EnvironmentVariablesManager::VariableContainer EnvironmentVariablesManager::PairToContainer(const std::string& name, const std::string& value) const { VariableContainer pair; std::copy(name.begin(), name.end(), std::back_inserter(pair)); pair.push_back('='); std::copy(value.begin(), value.end(), std::back_inserter(pair)); pair.push_back('\0'); return pair; }
Тесты через обычный assert.
Файл EnvironmentVariablesManager_unittest.cpp:
#include <iostream> #include <string> #include <vector> #include <cstdlib> #include <cstring> #include <cassert> #ifdef WINDOWS #include <windows.h> #else #include <stdlib.h> #include <stdio.h> #include <unistd.h> #endif #include "EnvironmentVariablesManager.h" void Test_EnvironmentVariablesManager_get_put() { EnvironmentVariablesManager env; assert(std::string("") == env.get("_a_unique_variable_")); env.put("_a_unique_variable_", "b"); assert(std::string("b") == env.get("_a_unique_variable_")); env.put("_a_unique_variable_", "abc"); assert(std::string("abc") == env.get("_a_unique_variable_")); env.put("_a_unique_variable_", ""); assert(std::string("") == env.get("_a_unique_variable_")); } namespace { std::string ReadEnvironmentVariableViaShell(const std::string& name) { #ifdef WINDOWS const std::string shell = EnvironmentVariablesManager::GetOSVariable("ComSpec"); assert(!shell.empty()); const std::string cmd = shell + " /c echo %" + name + "%"; FILE* const f = _popen(cmd.c_str(), "rb"); #else const std::string cmd = "echo $" + name; FILE* const f = popen(cmd.c_str(), "r"); #endif assert(f != NULL); std::vector<char> line(1024, 0); size_t read = 0; while (!::feof(f) && read < line.size()) { const size_t sz = ::fread(&line[read], 1, line.size() - read, f); read += sz; } #ifdef WINDOWS ::_pclose(f); #else ::pclose(f); #endif line.resize(read); std::string trimmed(read, '\0'); std::copy(line.begin(), line.end(), trimmed.begin()); return trimmed.substr(0, trimmed.find_last_not_of("\r\n") + 1); } } void Test_EnvironmentVariablesManager_put_is_propagated_to_child_process() { EnvironmentVariablesManager env; #ifdef WINDOWS const std::string empty = "%__unique_%"; #else const std::string empty = ""; #endif assert(empty == ReadEnvironmentVariableViaShell("__unique_")); env.put("__unique_", "b"); assert(std::string("b") == ReadEnvironmentVariableViaShell("__unique_")); env.put("__unique_", ""); assert(empty == ReadEnvironmentVariableViaShell("__unique_")); } void Test_EnvironmentVariablesManager_must_take_a_copy() { EnvironmentVariablesManager env; char var[] = "12345678"; env.put("var", var); assert(env.get("var") == std::string("12345678")); std::strcpy(var, "abc"); assert(env.get("var") == std::string("12345678")); } void Test_EnvironmentVariablesManager_del() { EnvironmentVariablesManager env; env.put("variable_to_delete", "123"); assert(std::string("123") == env.get("variable_to_delete")); env.del("variable_to_delete"); assert(env.get("variable_to_delete").empty() == true); } void Test_EnvironmentVariablesManager_IsOSVariableSet_set_and_unset() { EnvironmentVariablesManager env; env.put("a", "value"); assert(EnvironmentVariablesManager::IsOSVariableSet("a") == true); env.put("a", ""); assert(EnvironmentVariablesManager::IsOSVariableSet("a") == false); } void Test_EnvironmentVariablesManager_GetOSVariable() { const std::string unique_name = "EnvironmentVariablesManager_GetOSVariable"; assert(EnvironmentVariablesManager::GetOSVariable(unique_name.c_str()) .empty()); const std::string unique_name_pair = unique_name + "=12345678"; char var[1024]; unique_name_pair.copy(var, sizeof(var)); var[unique_name_pair.length()] = '\0'; ::putenv(var); assert(EnvironmentVariablesManager::GetOSVariable(unique_name.c_str()) == "12345678"); } void Test_EnvironmentVariablesManager_PutOSVariable() { const std::string unique_name = "EnvironmentVariablesManager_PutOSVariable"; const char* before = ::getenv(unique_name.c_str()); if (before != NULL) assert(std::string(before).empty()); const std::string unique_name_pair = unique_name + "=12345678"; char var[1024]; unique_name_pair.copy(var, sizeof(var)); var[unique_name_pair.length()] = '\0'; EnvironmentVariablesManager::PutOSVariable(var); const char* after = ::getenv(unique_name.c_str()); assert(after != NULL); assert(std::string(after) == "12345678"); } int run_tests(int argc, const char* const argv[]) { Test_EnvironmentVariablesManager_GetOSVariable(); Test_EnvironmentVariablesManager_PutOSVariable(); Test_EnvironmentVariablesManager_get_put(); Test_EnvironmentVariablesManager_put_is_propagated_to_child_process(); Test_EnvironmentVariablesManager_must_take_a_copy(); Test_EnvironmentVariablesManager_del(); Test_EnvironmentVariablesManager_IsOSVariableSet_set_and_unset(); return 0; } int main(int argc, const char* const argv[]) { run_tests(argc, argv); std::cout << "All tests pass." << std::endl; }
Ничего особо замысловатого, но удобно.
Посты по теме:
]]>Я начал с OMNI-395. Построен на Zilog Z180, имеет до 1М non-volatile памяти для хранения данных (журналов операций, например), AT-модем на 2400 (даже с какой-то коррекцией), двенадцативольтовые порты RS232 для PIN-клавиатуры, принтера и просто порт общего назначения (мы его использовали для подключения к мультипортовому шлюзу в TCP/IP) (увы, не все порты имеют полный набор линий RS232, что вынуждает изобретать программные велосипеды), ну и LCD экран.
Архитектура, кстати, интересная. Для выполнения пользовательского кода применялась виртуальная машина (видимо, так обходились ограничения в адресации, страницы и прочее). Это позволяло использовать для данных много памяти, но вот размер и скорость кода была весьма скромная. Приходилось, например, не использовать родной sprintf, а писать свой, чтобы избежать переполнение локального стека и т.д. Расчет табличного CRC по 5-10КБ данных работал видимые секунды.
Хотя, положа руку на сердце, по сравнению с терминалами других фирм в то время (Injenico, Nurit и т.д.), где часто надо было заниматься ерундой типа ручного переключения страниц памяти и там хранить журналы, среда VeriFone (TXO) давала почти стандартную библиотеку C, где с портами и файлами можно было работать через read/write/ioctl и т.д. А то, что шаловливые умы разработчиков обычно хотят запихнуть в несчастный девайс слона - и вызывало проблемы. Когда со временем ограничения были более менее освоены, разработка стала почти беспроблемной.
Компилятор С поддерживал почти всю стандартную библиотеку и раздельную компиляцию модулей. Загрузочные же модули (R-модули), можно было динамически запускать прямо из пользователького кода (типа механизма оверлеев или CHAIN в классических бейсиках).
И тут дед мороз принес списанный старенький OMNI-395, и радости моей не было предела. Откупоривать будем? Конечно!
Включаем, на всякий случай, и, о чудо, там еще загружена одна из моих прошивок.
Итак, вид изнутри.
Два платы - верхняя, под клавиатурой, и нижняя, с разъемам.
Раскопаю компилятор и загрузчик в сусеках, попробую написать что-нибудь.
Увы, описания деталей аппаратной части я не нашел, а официальная документация описывает только стандартную библиотеку и немного виртуальную машину, в коды которой компилируется пользовательский код.
Конечно, OMNI-395 можно спокойно использовать в качестве НЕДО-PC как он есть. Например, как контроллер чего-либо. Если нужны порты общего назначения, то подходит RS-232 (DTR/RTS – вывод, CTS/DSR – ввод).
А идеале можно диассемблировать прошивку (там всего 64КБ), и поняв архитектуру, уже писать чисто на Z80, и тогда работать будет гораздо быстрее.
Надо сказать, что, несмотря на смену аппаратной платформы и компилятора (не один раз), у VefiFone отличная приемственность интерфейса библиотек, что позволяет портировать прошивки на последующие модели с минимальным изменениями. После 395-го я еще поработал с OMNI-3350, 3750, VX510, VX610.
Кстати, по тэгу #cardpayments в Твиттере я выкладываю фотографии ПОСов, пинпадов и прочих устройств приема банковских карточек, если кому интересно.
]]>
Максимальный размер одного окна 100x50 символов при 64 цветах, и можно
одновременно создавать до 9 окон. Информация в каждое окно выводится
независимо. Управляющие команды передаются через специальные
^[
-последовательности.
В целом – очень удобно для проектов на микроконтроллерах, когда лень возиться с выводом VGA самостоятельно, а вывод в формате RS-232 можно написать на коленке. Я также подключал это к Raspberry Pi. Единственное, могу сказать, что на скорости более 9600 лучше поддержать сигнал CTS, иначе символы могут пропадать, так как девайс не успевает.
]]>
Там хоть и исходники на экране, но разглядеть что-либо конкретное будет трудно.
]]>На данный момент я собрал 20 чипов (Intel, AMD, National Semiconductor, NEC, Samsung, Texas Instruments, и также в произведенных Советском Союзе и Чехословакии). Среди тех, что имеют даты выпуска на корпусе, самый старый 1974 года, а самый новый - 1980. Все чипы, за исключением одного, прекрасно работают. Тестировал я их в Радио-86РК.
Проверка показала, что все чипы, кроме произведенных AMD, совершенно одинаковы по показаниям 8080 CPU Exerciser. Кристаллы же AMD, AM8080 and AM9080A, имеют одно небольшое отличие в команде битовой операции AND (инструкции ANA и ANI). Следуя оригинальному поведению процессоров i8080 от Intel, флаг дополнительного переноса AC должен вычисляться как третий (A3) бит результата операции OR между аккумулятором и аргументом команд ANA или ANI. Процессоры же AMD просто обнуляют флаг AC в инструкциях ANA и ANI. Сложно представить, почему Intel придумал столь странную логику для флага AC в операциях AND (в других битовых операциях флаг AC просто обнуляется аналогично логике от AMD).
Двойным кликом можно переключать верх и низ чипов.
Интересно, что изначально AMD начала производить клоны i8080, используя, дословно, украденные у Intel чертежи. Так что война Intel vs AMD началась именно тогда.
Этот чип, увы, не работает.
Родной вкладыш, с которым продавался ВМ80: страница 1 и страница 2.
ВСЕ! (пока).
А вот как я все это фотографировал при помощи айфона и двух Raspberry Pi’ев.
Хочу надеяться, что коллекция будет расти. Увы, пока я у меня только один чип в белой керамике. И если вы знаете о существовании еще каких-то клонов i8080 от еще каких-то фирм, буду признателен за информацию.
Также с удовольствием приму в подарок любой i8080 (даже не обязательно рабочий) и могу поставить ссылку на подарившего.
]]>В игре, ясное дело, ничего особенного, просто понравилась она мне. К тому же попробовал ascii.io для записи asciicast’a, так что геймплей можно заценить - http://ascii.io/a/1715
Исходник есть на GitHub’е, но можно просто скопировать снизу.
// Originally taken from http://itblogs.org/c-konsolnaya-zmejka/. #include <iostream> #include <cstdio> #include <cstdlib> #include <ctime> #ifdef WINDOWS // Библиотека, нужна для использования функции Sleep(). #include <windows.h> // Библиотека, нужна для использования функций kbhit() и getch(). #include <conio.h> #else #include <unistd.h> #include <termios.h> #include <sys/select.h> #define STDIN_FILENO 0 #define NB_DISABLE 0 #define NB_ENABLE 1 #define Sleep(x) usleep(x*1000) int kbhit() { struct timeval tv; fd_set fds; tv.tv_sec = 0; tv.tv_usec = 0; FD_ZERO(&fds); FD_SET(STDIN_FILENO, &fds); select(STDIN_FILENO+1, &fds, NULL, NULL, &tv); return FD_ISSET(STDIN_FILENO, &fds); } void nonblock(int state) { struct termios ttystate; // Get the terminal state. tcgetattr(STDIN_FILENO, &ttystate); if (state == NB_ENABLE) { // Turn off canonical mode. ttystate.c_lflag &= ~ICANON; // Minimum of number input read. ttystate.c_cc[VMIN] = 1; } else if (state == NB_DISABLE) { // Turn on canonical mode. ttystate.c_lflag |= ICANON; } // Set the terminal attributes. tcsetattr(STDIN_FILENO, TCSANOW, &ttystate); } int getch() { return fgetc(stdin); } #endif // snake_size - размер змейки. // change_x, change_y - в какую сторону движется змейка. // coordinates_x[1000], coordinates_y[1000] - массивы, хранящие координаты // частей тела змейки. // Kоординаты головы змейки хранятся в coordinates_x[1], coordinates_y[1]. // food_x, food_y - координаты еды. int snake_size, change_x, change_y, coordinates_x[1000], coordinates_y[1000]; int food_x = -1, food_y = -1; // symbol - хранит в себе ASCII код нажатой клавиши. // a[1000][1000] - наша таблица, в которой происходит вся игра. char symbol, a[1000][1000]; // Константы: // N - размер таблицы, а именно высота. // M - ширина таблицы. // INTERVAL - интервал в миллисекундах, через каждый этот промежуток // времени змейка будет передвигаться. const int N = 13, M = 17, INTERVAL = 200; // функция, считывающая нажатую клавишу. void change_direction() { // Cчитываем нажатую клавишу с помощью функции getch(). symbol = getch(); switch (symbol) { // Управление змейкой у нас через wasd. case 'w': if (change_x != 1 || change_y != 0) { change_x = -1; change_y = 0; } break; case 'a': if (change_x != 0 || change_y != 1) { change_x = 0; change_y = -1; } break; case 's': if (change_x != -1 || change_y != 0) { change_x = 1; change_y = 0; } break; case 'd': if (change_x != 0 || change_y != -1) { change_x = 0; change_y = 1; } break; #ifndef WINDOWS case 'q': nonblock(NB_DISABLE); std::exit(0); #endif default: break; } } // функция для вывода таблицы void show_table() { // Очищаем консоль. #ifdef WINDOWS system("cls"); #else system("clear"); #endif // Выводим таблицу и края. for (int i = 0; i <= N + 1; ++i) for (int j = 0; j <= M + 1; ++j) std::cout << (i == 0 || j == 0 || i == N + 1 || j == M + 1 ? '#' : a[i][j]) << (j <= M ? "" : "\n"); } // Очищаем координаты, в которых располагалась змейка. void clear_snake_on_table() { for (int i = 1; i <= snake_size; ++i) a[coordinates_x[i]][coordinates_y[i]] = ' '; } // Красим координаты змейки. void show_snake_on_table() { // Изменяем тип головы. if (change_x == 1 && change_y == 0) a[coordinates_x[1]][coordinates_y[1]] = 'v'; if (change_x == -1 && change_y == 0) a[coordinates_x[1]][coordinates_y[1]] = '^'; if (change_x == 0 && change_y == 1) a[coordinates_x[1]][coordinates_y[1]] = '>'; if (change_x == 0 && change_y == -1) a[coordinates_x[1]][coordinates_y[1]] = '<'; // Красим змейку. for (int i = 2; i <= snake_size; ++i) a[coordinates_x[i]][coordinates_y[i]] = '@'; } // Проверяем, съела ли змейка саму себя. bool game_over() { for (int i = 2; i <= snake_size; ++i) // Eсли координаты головы змейки равны координате какой-либо части тела // змейки, то змейка съела саму себя. if (coordinates_x[1] == coordinates_x[i] && coordinates_y[1] == coordinates_y[i]) return true; // Если все координаты различны, то все в порядке - играем дальше. return false; } // Проверяем, не вышла ли змейка за поле, если да то возвращаем ее обратно. void check_coordinates() { if (coordinates_x[1] > N) coordinates_x[1] = 1; if (coordinates_x[1] < 1) coordinates_x[1] = N; if (coordinates_y[1] > M) coordinates_y[1] = 1; if (coordinates_y[1] < 1) coordinates_y[1] = M; } // функция следующего хода, в которой наша змейка сдвигается в сторону // на 1 ячейку. void next_step() { // Чистим таблицу от змейки. clear_snake_on_table(); // Передвигаем тело змейки. for (int i = snake_size; i >= 2; --i) { coordinates_x[i] = coordinates_x[i - 1]; coordinates_y[i] = coordinates_y[i - 1]; } // Передвигаем голову змейки. coordinates_x[1] += change_x; coordinates_y[1] += change_y; // Проверяем в порядке ли координаты. check_coordinates(); // Если голова змейки там же где и еда, то увеличиваем размер змейки // и очищаем координаты змейки. if (coordinates_x[1] == food_x && coordinates_y[1] == food_y) { snake_size++; food_x = -1; food_y = -1; } // Рисуем змейку. show_snake_on_table(); // Если змея укусила себя. if (game_over()) { // Cообщаем всю правду о игроке. std::cout << "You're looser! \n"; // Приостанавливаем игру. #ifdef WINDOWS system("pause"); #endif // Завершаем программу. std::exit(0); } } // функция проверки на наличие еды на карте. bool food_check() { // Если координаты еды неопределенны, то возвращаем true. if (food_x == -1 && food_y == -1) return false; // В остальных случаях false. return true; } // функция добавления еды на карту. void place_food() { std::srand(std::time(NULL)); // Cтавим в рандомное место еду. for (int i = 1; i <= 9; ++i) { int x = std::rand(), y = std::rand(); if (x < 0) x *= -1; if (y < 0) y *= -1; x %= (N + 1); y %= (M + 1); if (x == 0) ++x; if (y == 0) ++y; if (a[x][y] != '@' && a[x][y] != 'v' && a[x][y] != '^' && a[x][y] != '<' && a[x][y] != '>') { food_x = x; food_y = y; a[x][y] = '+'; return; } } } // Начальные установки. void standart_settings() { // Размер змеи - 2. snake_size = 2; // Змейка занимает две клетки вправо от координаты {1,1}. coordinates_x[1] = 1; coordinates_y[1] = 2; coordinates_x[2] = 1; coordinates_y[2] = 1; // Змейка движется вправо. change_x = 0; change_y = 1; } int main() { // Задаем стандартные настройки. standart_settings(); #ifndef WINDOWS std::memset(a, ' ', sizeof(a)); nonblock(NB_ENABLE); #endif // Бесконечный цикл. while (true) { // Если нажата клавиша, обрабатываем нажатую клавишу. if (kbhit() != 0) change_direction(); // Двигаем змейку. next_step(); // Если нет еды, то ставим ее. if (!food_check()) place_food(); // Рисуем змейку. show_table(); // "Усыпляем" программу на заданный интервал. Sleep(INTERVAL); } }
Если у кого есть всякие консольные примочки типа этой - делитесь, не стесняйтесь.
]]>Недавно автор выпустил новую, цветную версию, и данный обзор посвящен именно ей. Итак, возможности цветного Maximite (новые и улучшенные возможности отмечены звездочкой):
Встроенный Бейсик позволяет максимально полно использовать все эти возможности. Можно даже создавать обработчики прерываний таймера в виде подпрограмм.
Я, конечно, приобрел конструктор цветного Maximite, как и в прошлый раз у Altronics.
Все элементы с ножками, чтобы, видимо, дилетантам в пайке, типа меня, было проще. Только один конденсатор (C10) там планарный и довольно маленький. Хоть под него пятачки были уже с оловом, пришлось поерзать с пайкой, чтобы не закоротить.
В сборе.
Для сравнения старый и новый Maximite.
Вот, что можно делать с цветами на Бейсике.
А это хоть и не в цвете, что, все-таки, классика.
Как пишет автор, возможность генерировать цветной видео сигнал потребовала 100-ногую модель PIC32, где есть три канала SPI. Понятно, что при этом требуется “прогонять” в три раза больше данных, а так так частота кристалла осталась той же, 80МГц, то объективно, цветная прошивка работает немного медленнее, чем оригинальная черно-белая. Поэтому для нового Maximite есть вариант чисто черно-белой прошивки, в которой аналогично добавлены все новые периферийные возможности, и которая работает по скорости аналогично предыдущему, черно-белому Maximite’у.
Конструктор от Альтроникс, как и в прошлый раз, отличного качества.
В целом, рекомендую.
]]>Как следует из названия - это книга о “новом” C. Конкретно: С89, C99 и даже C11. Плюс, краткий обзор основных “игроков” типа gcc или clang.
Скажу сразу, что это не учебник, а сборник советов автора на тему использования возможностей современного C.
Зацените начало введения:
C has only a handful of keywords and is a bit rough around the edges, and it rocks. You can do anything with it. Like the C, G and D chords of a guitar, you can learn the basic mechanics pretty quickly, and then spend the rest of your life getting better.
Мне понравилось. А когда я увидел главу под названием “C syntax you can ignore”, точно решил с книгой ознакомиться.
Итак, сначала идет небольшой обзор стандартов. Затем пара глав про утилиты и средства разработки (gdb, autotools, makefiles, valgrind, git). Кратко, но для новичков это будет началом, где копнуть. Далее идет, собственно, про язык. Дается много полезных советов про указатели, структуры (понятно, что для “нового” С), приемчики объектно-ориентированного подхода, работа с Unicode. В конце обзор нескольких распространенных библиотек.
Книга выглядит немного поверхностно, но на одно вечернее чтиво тянет спокойно.
Например, что лично нового узнал для себя:
Также, например, если надо из функции вернуть несколько значений, можно просто использовать struct в качестве результата функции. Если размер структуры небольшой, то все будет и красиво и быстро. Не то, чтобы я этого не знал, но просто в С это не самый распространенный прием, и часто о нем забываешь.
Как всегда бескомпромиссный совет использовать makefile вместо пачки скриптов.
Но книга была бы пресной без “ЧТО?! - НИКОГДА!!!”.
Например, автор поддерживает использование goto для экстренного выхода из функции, когда в ее конце есть код завершения, который надо обязательно выполнить. Лично я так никогда делать не буду, а постараюсь изолировать этот код, например, в отдельной функции, и вызывать ее на каждом подобном выходе.
Также автор рекомендует не использовать switch в целом, объясняя это его ненаглядным синтаксисом и проблемой пропущенного break. Опять, лично я с этим не могу согласиться.
Далее автор всячески рекомендует autotools (autoconf, automake, libtools). Лично до сих пор не могу до конца понять суть этих сложных и запутанных систем. Хорошо, когда скрипт “configure” уже кем-то написан, но делать его самому…
Под занавес - doxygen. Крайне редко я видел проекты, где автоматическая документация реально хорошего качества. Увы, поддержание doxygen’овских вставок в достойном состоянии - это отдельный фронт работы, на который обычно нет ни желания, ни времени, если, конечно, это документация не является коммерческой частью продукта.
Вывод: Можно купить книгу в офис, по очереди пролистать за вечером, и затем можно перетирать детали на офисной кухне с коллегами.
]]>Теперь небольшой fix для Хабра. Было:
Стало (просмотр в полную ширину и без рекламы):
Фикс можно приложить расширением Stylebot.
#topline { display: none; } #header .logo { background: none; height: 30px; } #header .main_menu .banner_special { display: none; } .sidebar_right { display: none; } .content_left { width: 99%; } .rotated_posts { display: none; }]]>
Для Splunk логи - это текстовая информация, разбитая на строки. В процессе индексирования строки логов разбиваются на поля, например “имя=значение”, хотя это настраиваемо. Далее с помощью специального языка запросов SPL можно работать с этими полями: сортировать, агрегировать, создавать вычисляемые поля, формировать таблицы, обращаться ко внешним словарям, например, из SQL-базы, и, конечно, строить разнообразные графики. SPL работает не только с одиночными строками, но и позволяет группировать, “сжимать” логически в одну строку многострочные куски.
Как заявляет сам Splunk, все логи, что хранятся в системе за любое время, являются доступными для запросов, то есть нет понятия архивирования. Конечно, машин (машины), где крутится Splunk должна сообразно отражать объем хранимой и обрабатываемой информации.
Еще Splunk называет себя “Google для логов”, но оставим это высказывание без комментариев на их собственный откуп.
Интерфейс Splunk - веб. Можно создавать панели (dashboard’ы), из которых формировать свое собственное Splunk-приложение. У Splunk есть магазин приложений (хотя большинство из них бесплатны), где есть море уже готовых конфигураций для анализа популярных систем, например, UNIX syslog, логи Apache, Microsoft Exchange и т.д.
Программный комплекс Splunk можно бесплатно скачать с официального сайта. Лицензирование происходит на основе дневного объема прокачиваемых через систему логов. Для ознакомления есть минимальный бесплатный объем, которого прекрасно хватает для освоения системы.
Для быстрого ознакомления рекомендую книгу Exploring Splunk от создателей этого продукта. Пролистав ее, вы получите неплохое начальное понимание, что можно делать в Splunk, суть языка запросов SPL и т.д.
В данной же статье я хочу показать реальный пример, который можно повторить в течение получаса. Для этого достаточно бесплатно скачать и установить Splunk под вашу операционную систему. Далее можно просто следовать моим инструкциям.
Пример будет необычным. Традиционно, логи используются для анализа исторической информации. Но ничто не мешает извлекать ее по ходу появления и создавать “живые” индикаторы на ее основе. Соглашусь, мой пример немного искусственен, но я хочу показать, как быстро и просто можно “накидывать” данные в Спланк, формализовать их и строить на их основе динамический пользовательский интерфейс
Вот простой скрипт, который в течение минуты будет записывать в лог сообщения, содержащие процентный показатель (от 0 до 100%).
require 'date' duration = 60*1 update_period = 0.5 i = 0 while i <= duration do progress = i * 100.0 / duration msg = "%s progress=%05.2f\n" % [DateTime.now, progress] puts msg open("logs/my.log", 'a') { |f| f << msg } i = i + update_period sleep update_period end
Лог будет выглядеть примерно вот так:
2012-11-23T15:58:54+00:00 progress=45.00
2012-11-23T15:58:55+00:00 progress=45.83
2012-11-23T15:58:55+00:00 progress=46.67
2012-11-23T15:58:56+00:00 progress=47.50
2012-11-23T15:58:56+00:00 progress=48.33
2012-11-23T15:58:57+00:00 progress=49.17
2012-11-23T15:58:57+00:00 progress=50.00
Наша цель создать dashboard (панель просмотра) в Спланке, которая отображала бы процесс выполнения скрипта в форме красивого индикатора, беря при этом данные из лога.
Для простоты мы будет все делать на одной машине, и Спланк будет брать лог просто из указанного файла.
Итак, вы уже установили Спланк, и если зайти на “http://localhost:8000”, то вы увидите рабочую страницу Спланка, где можно залогиниться пользователем “admin”.
Далее идем по меню: “Manager -> Data Inputs -> Add data -> A file or directory or files”. Тут мы указывает имя файла или каталога, где лежат наши логи (в данном случае это будет одиночный файл).
Подтверждаем создание так называемого источника логов (source type). Как я уже говорил, логи в Спланк могут поступать различными путями. Каждый такой источник логов именован и может обрабатываться независимо.
Итак, лог добавлен. Мы видим, что Спланк “подцепил” файл и уже разобрал строки на поля. Спланк понимает огромное количество форматов даты и времени, но и это настаиваемо.
Далее мы даем имя нашему источнику логов - “test_logging”, и сохраняем настройки.
Возвращаемся на главную страницу и строке Search вводим наш первый запрос на языке SPL (в красном):
sourcetype="test_logging" | table progress as float
Перевожу: взять логи из источника “test_logging”, сформировать таблицу, в которую добавить колонку со значением из поля “progress”, попутно преобразовав его в тип float. Внизу (в синем) мы видим результат выполнения запроса (в логе уже есть данные). В SPL работает принцип UNIX pipes (|), когда результат одной операции передается на вход следующей.
Итак, таблица есть. Теперь сделаем ей графическое представление. Так как у нас всего одно поле, показывающее процент выполнения от 0 до 100, то, например, можно использовать вид спидометра, стрелка которого будет демонстрировать текущее значение. Кликаем на “Formatting options” (в синем) и выбираем тип элемента (Chart type) “radial gauge” (в красном). Появляется вот такой красивый спидометр.
Первый виджет готов. Теперь ради эксперимента создадим еще один. Он тоже будет показывать значения поля progress, но в виде горизонтального индикатора (progress bar), двигаясь слева направо. Запрос в этом случае будет таким:
sourcetype="test_logging" | table _time progress | head 1
Что значит: по данным из источника “test_logging” создать таблицу из двух полей, “_time” и “progress”, из которой брать только первую строку. Сортировка по умолчанию по полю “_time” по убыванию. Внизу (в зеленом) мы видим результат этого запроса.
Далее кликаем на “Formatting options”, выбираем тип “bar” (в зеленом), для оси Y задаем интервал от 0 до 100. Почему-то тут ось X идет по вертикали (там будет отображаться поле “_time”), а ось Y по горизонтали (там будет отображаться поле “progress”). Так как запрос на предыдущей картинки показывал значение 100, то и наш горизонтальный индикатор полностью закрашен.
Я пропустил это для первого виджета, спидометра, но отлаженный запрос и его визуальное представление можно сохранить в виде “Dashboard panel”. Для это кликаем на “Create” и “Dashboard panel…” (в красном) и сохраняем настройки. Назовем первый наш виджет “Speedometer”, и второй “Progress bar”.
При сохранение первого виджета будет предложено также создать панель (dashboard), назовем ее “Test logging”. При сохранении второго виджета добавим его уже созданную панель.
После создания панели можно кликнуть на “Dashboards & Views”, выбрать ее по имени “Test logging”. Внешний вид будет примерно таким:
Пока данных нет, поэтому панели пустые. Мы видим название панели (в красном), названия виджетов (в желтом), кнопку активации панели (в зеленом) и кнопки “Edit” (в синем), которой можно налету подправить запрос или визуальное представление. Перед запуском скрипта надо через кнопку “Edit” зайти в каждый из виджетов и указать интервал времени обновления в одну секунду: от “rt-1s” (real time - 1s), до “rt” (текущее время).
Итак, все! Зажимаем на “On” и запускаем наш скрипт.
Видео как это работает:
Все!
Конечно, пример очень простой, но надеюсь, мне удалось передать хотя бы минимальное ощущение, как работает Спланк.
Скажу, мы в компании недавно начали использовать этот продукт и пока довольны. В этом году я был на конференции SplunkLive, где выступали люди из весьма солидных контор типа British Telecom. С помощью Splunk они обрабатывают гигабайты логов ежедневно.
Увы, даже у крупных контор не всегда есть желание разрабатывать подсистему для обработки логов, так что Спланк может прийтись очень кстати.
]]>Основные отличительные особенности Пропеллера:
Язык Spin разработан для удобного многопроцессорного программирования, и выглядит как нечто среднее между процедурным и объектно-ориентированным языком.
Вот пример кода на Spin, запускающего функцию крутиться на нескольких ядрах. Код реально простой и понятный.
CON
_clkmode = xtal1 + pll16x 'Establish speed
_xinfreq = 5_000_000 '80Mhz
OBJ
led: "E555_LEDEngine.spin" 'Include LED methods object
VAR
byte Counter 'Establish Counter Variable
long stack[90] 'Establish working space
PUB Main
cognew(Twinkle(16,clkfreq/50), @stack[0]) 'start Twinkle cog 1
cognew(Twinkle(19,clkfreq/150), @stack[30]) 'start Twinkle cog 2
cognew(Twinkle(22,clkfreq/100), @stack[60]) 'start Twinkle cog 3
PUB Twinkle(PIN,RATE) 'Method declaration
repeat 'Initiate a master loop
repeat Counter from 0 to 100 'Repeat loop Counter
led.LEDBrightness(Counter, PIN) 'Adjust LED brightness
waitcnt(RATE + cnt) 'Wait a moment
repeat Counter from 100 to 0 'Repeat loop Counter
led.LEDBrightness(Counter,PIN) 'Adjust LED brightness
waitcnt(RATE + cnt) 'Wait a moment
Функция cognew
запускает задачу на трех ядрах, параметризируя каждую
своей частотой и стеком.
Упрощенно Пропеллер устроен следующим образом:
Название “Пропеллер” произошло от его модели передачи приоритета на доступ к разделяемым ресурсам. Модуль Hub, контролирующий разделение времени, делает это по кругу, типа крутящегося пропеллера.
Я не хочу в этой статье углубляется в сам Пропеллер, ибо это большая тема. Для интересующихся в конце есть ссылки на книги, в которых можно получить исчерпывающую информацию об этом микроконтроллере.
Но хочу рассказать об одном интересном проекте, который называется “Pocket Mini Computer”. Это мини-компьютер на базе Пропеллера (P8X32A), использующий evaluation board “P8X32A QuickStart” как основу.
Выглядит это добро следующим образом (фотография с официального сайта):
Фактически, автор продает evaluation board плюс плату расширения, на которой есть VGA, microSD, PS/2, звук и Wii Gameport. Опционально можно поставить микросхему оперативки SRAM на 32КБ.
Фишка проекта в том, что автор разработал интерпретатор Бейсика, который превращает все это в микро-компьютер а-ля 80-е. Бейсик написан на Spin’e (исходники открыты). Диалект весьма ограничен, например, нет массивов, строковых и вещественных переменных, имена переменных только однобуквенные и т.д. Но тем не менее, дается доступ ко всей периферии, включая SD-карту, и также позволяет запускать чисто двоичные файлы, которые могут быть написаны хоть на том же Spin’e, хоть на С (Parallax имеет версию GCC для Пропеллера), хоть на ассемблере.
Далее несколько фотографий конструктора, чтобы было понятно, что дается в наборе. Как я уже говорил, основа PMC - это готовая плата “P8X32A QuickStart”, поэтому спаять надо только плату расширения.
Почти все запаяно.
Бутерброд в сборе.
Вот небольшая демка, чтобы оценить графические возможности.
Не получается назвать его процессором общего назначения. По моему субъективному мнению, для эффективного использования Пропеллера надо очень хорошо понимать свою прикладную задачу. Например, у Пропеллера нет ШИМ, ЦАП/АПЦ, встроенной флеш-памяти, триггеров, понятия прерываний, и создатели предлагают либо реализовывать необходимое программно, используя силу нескольких ядер, либо использовать специализированные внешние микросхемы. В книгах, приведенных в конце, описано множество примеров работы с дополнительными микросхемами.
Интересно другое. Создатели Пропеллера не забоялись отойти от традиционного подхода и попытались вложить в кристалл конкретные прикладные возможности, почти готовые задачи. Может для каких-то проектов это придется очень кстати. Как я понял, Пропеллер очень удобен для создания разного рода игровых автоматов и приставок, например, из-за встроенной возможности генерировать качественный телевизионный и VGA сигнал.
Вывод: интересная архитектура, определенно заслуживающая внимания.
Опять-таки, двоякое ощущение. Вроде работает, но ресурсов Бейсику явно не хватает, особенно памяти. Например, тот же Maximite на базе PIC32 его на голову превосходит. На нем можно запустить хоть RetroBSD, хоть Радио-86РК. Да и встроенный MMBasic несравнимо мощнее.
Хотя, за 39 долларов США - это отличная игрушка для тех, кто хочет пощупать Пропеллер, имея уже собранное устройство.
Книги по Пропеллеру, которые я прочитал, что касаемо архитектуры, и проглядел (что касаемо проектов). Все рекомендую.
Небольшая и очень понятная книга для начинающих. Описаны (с картинками) интересные проекты. Один из соавторов является конструктором PMC.
Getting Started With the Propeller
Крайне грамотная книга в плане архитектуры и понимания сути Пропеллера. В ней рассматривается только программирование на Spin, но с полным объяснением подходов и особенностей микроконтроллера. Прочитав первую главу, вы получите почти полное понимание архитектуры. Далее описаны несколько проектов (это можно пропустить).
Programming the Propeller with Spin : A Beginner’s Guide to Parallel Processing (Tab Electronics)
Сборник реальных проектов с использованием Пропеллера от его создателей.
Programming and Customizing the Multicore Propeller Microcontroller : The Official Guide
На момент написания этой статьи, первая книга доступна только в формате Киндл на Амазоне по цене в два доллара, а вот вторую и третью можно найти, если поискать.
]]>Небольшое лирическое отступление.
Кстати, все знают, что Нортон Коммандер написал не Питер Нортон? Его в одиночку, по крайне мере до канонической версии 3 включительно, писал и поддерживал товарищ Соухэ, применяя, как он сам говорил, экстремальный на то время подход, смешивая С и ассемблер. Это позволяло быстро разрабатывать эффективные программы, тогда как “тру”-программисты того времени писали исключительно на ассемблере.
Кстати, все знают, что “за бугром” синий текстовый экран с двумя панелями называют “доисторической русской программой”? Забавно, что проникновение Коммандера в умы компьютерщиков на территории бывшего СССР имеет несравнимые масштабы по отношение к тем местам, где Коммандер родился.
Ладно, вернемся к книге. Мне понравилась тема покупки старых книг практически за бесценок, просто чтобы подержать их в руках.
Снова радость - списанный библиотечный экземпляр, проклеенный скотчем.
Я не люблю, когда пишут в книгах или загибают листы, но выглядит это трогательно. Кто-то это явно зубрил.
Ну как можно после такой иллюстрации не понять, как работает EQU?
Или DUP.
Или стек.
Или как перехватывать прерывания в ДОСе.
Кстати, это оказалось третье издание книги, в котором очень кратко описывался защищенный режим 286 и 386-х.
В общем, книга отличная.
На протяжении всей книги описывается процесс создания визуального посекторного редактора диска (дискеты или винчестера). В конце вы получаете весьма нетривиальную программу приличного размера. Начав с самых азов ассемблера, придется освоить арифметические операции, работу с прерываниями БИОС и ДОС, прямой доступ в экранную область, клавиатуру. Но самое главное, появляется понимание как писать действительно большие программы на ассемблере. Для меня, например, было реальным открытием, что если каждая подпрограмма будет по умолчанию сохранять регистры, кроме используемых для передачи и возврата данных, то писать становится разительно проще. Конечно, сейчас это знание для детского сада, но в детском саду должен быть кто-то, кто это донесет.
В конце приводится полный откомментированный листинг этого редактора.
Еще в третьем издании добавлена отличная глава про смешивание ассемблера с языками типа С или Паскаля. В деталях разжевывается передача параметров и использование ассемблерных вставок. На десерт в одном из приложений дается исходник библиотеки с недвусмысленным названием SOCHALIB. В ней много разных функций для работы с экраном, мышью, клавиатурой. А еще там есть некоторые функции с префиксом Fast в имени. Например, те, что работают с экраном естественно не используют БИОС, а работают напрямую с экранной областью. Я был приятно удивлен, когда функция вывода символа на экран вместо умножения Y*80 для вычисления базы строки использовала таблицу (pre-calc) с уже посчитанными значениями. Не удивлюсь, если код этой библиотеки живет где-то в коде Коммандера или Утилит Нортона.
Кстати, если кому интересно, вот русский перевод первого издания.
Нортон П., Язык ассемблера для IBM PC
]]>
Хотите оценить прикольность девайса?
Итак, оно приехало:
Детали конструкции:
Начинаем послойное “введение магнезии”:
После первых двух слоев пристраиваем плату RPi (помним, что ее конструкция не имеет даже дырок под стойки):
Далее по слоям:
Берем винты, гайки и ключ…
Вуяля, девайс в сборе:
Если честно – я обожаю Raspberry Pi. Сложно себе представить удобнее девайс для школ и даже институтов в качестве учебного пособия. С ним может тягаться разве что Maximite, если речь идет о более “железячной” работе.
До этого мой “корпус” для RPi от SK Pang выглядел так:
Офигительно удобная штука, когда нужно иметь быстрый доступ к порту общего назначения (GPIO), но Pibow бескомпромиссен как обычный корпус.
]]>BEGIN
if (Hi(Code) == 0)
BEGIN
BAsmCode[0] = Lo(Code); CodeLen = 1;
return True;
END
else if (MomCPU <= CPU1802)
BEGIN
WrError(1500);
return False;
END
else
BEGIN
BAsmCode[0] = Hi(Code); BAsmCode[1] = Lo(Code); CodeLen = 2;
return True;
END
END
(взято из code1802.c).
Если честно, то сходу не совсем понятно, что это за язык. Это, конечно, С, но изначально исходник был на Паскале, потом автор решил перейти на С, оставив основную часть кода практически без изменений.
Проект называется “Macro-assembler AS”, автор Альфред Арнольд. Я его использую как макро-ассемблер для Intel 8080, который собирается на Маке, Линуксе и Windows. Лучшего я пока ничего не нашел.
Как пишет сам автор, изначально проект был на Турбо-Паскале, но потом автор был вынужден перейти на С, так как Борланд остановил развитие досовских версий, и нормального Паскаля для Линукса тогда еще тоже не было. В итоге автор сконвертировал исходники в С и продолжил развивать проект в нового ключе. Несмотря на всю сомнительность затеи, по мне, сделал он это весьма удачно, и проект не умер, как это часто бывает в случаях полного переписывания большого проекта. Но хотя я склонировал проект для себя, собрал на Маке и радостно использую, не испытываю жгучего желания что-либо контрибьютить, так как для соблюдения стиля надо писать на этом гремучем Паскале-Си. Увы, это неизбежное следствие конверсии, иначе проект превратится в помойку. Если поелозить по исходникам, можно найти много интересных трюков типа “как сделать так, чтобы Си работал как Паскаль”.
Но, повторюсь, всяческих успехов автору, так как проект жив и развивается. Кстати, у автора есть отличная коллекция различного железа, которой можно реально позавидовать.
У нас в компании была похожая ситуация. Часть нашего продукта - это бейсикоподобный язык (DSL), ориентированный на работу с базами данных. На нем пишется бизнес-логика банковского продукта.
Язык был разработан не вчера и поэтому немного старомоден. Но на нем написаны мириады строк кода лидирующего на рынке продукта. Разные попытки “заменить” язык на что-то “более новое и современное” и затем сконвертировать основную кодовую базу, к счастью, не получили хода. Сейчас мы прекрасно компилируем наш язык в Си, Яву и даже .NET, и очень, очень постепенно вносим крайне взвешенные незначительные изменения в сам язык. И это работает удивительно хорошо.
]]>Introduction to 8080-8085 Assembly Language Programming (Self-teaching Guides)
Резонный вопрос - для чего? Особенно в свете того, что за последний месяц я с нуля переписал два эмулятора Intel 8080, один на С, второй на JavaScript. Неужели у меня есть вопросы в понимании команд этого процессора?
Если честно, про эту книгу я просто слышал просто как о классном примере учебника по ассемблеру, что-то в стиле нетленки Морера про ассемблер 6502 для компьютеров Эппл. Можно было бы просто найти скан, ибо книга аж 1981 года, но на Амазоне был ее бумажный вариант всего за 300р вместе с доставкой, Б/У, разумеется.
Не знаю почему, но когда держу подобные книги в руках, то испытываю как-то
детский восторг, особенно от трогательно подрисованных пояснений,
старомодных шрифтов набора кода и записей в форме перфокарт.
А по сути книга действительно классная. Если вдруг, почему-то, вам захочется изучить ассемблер 8080, но вы никогда о нем не слышали, да и о программировании на ассемблере в целом, то эта книга как раз для этого.
Все начинается “от печки” в виде систем исчисления, и затем подробно рассматриваются группы команд процессора и примеры применения – индексирования массивов, умножения/деление, двоично-десятичное представление, концепция стека и т.д. В каждой главе есть набор мини-задачек и вопросов для самопроверки.
Очень хорошая книга.
]]>
Вы уже догадались, что это за красавец?
Да! Он самый!
Купил я его тут непосредственно у автора.
Мой экземпляр, кстати, собран на родных интеловских чипах, поэтому будет интересно запустить CPU Exerciser и посмотреть, будет ли он отличаться по результатам от КР580ВМ80A.
Я, конечно, не спец, но внешне плата спаяна классно, а в корпусе смотрится вообще супер, за что автору большое спасибо.
Сам проект подробно описан тут - radio86rk.pbworks.com.
На самом деле на сорока двух дюймах реально вставляет.
Записал небольшой видос:
Так как сейчас нет никакой возможности ничего залить на этот РК, обдумываю способы коммуникации. Например, можно сделать эмулятор магнитной ленты, чтобы файлы брались с карточки, например. Вот так, кстати, выглядит сигнал записи на ленту.
В целом впечатления от проекта отличные. Единственное, что мне не очень нравится – это реализация клавиатуры на микроконтроллере. Но увы – без этого нельзя было бы использовать готовую PS/2, а пришлось бы собирать “родную” на кнопках. Работы было бы больше, но однозначно добавило бы проекту винтажности.
Я попробую что-нибудь замутить в качестве расширения для Монитора, благо там стоит восьми килобайтная ППЗУ, поэтому сам Монитор можно не трогать.
Сразу скажу, что не буду отвечать на вопрос, сколько все это мне стоило. Ибо, дорого. Особенно с учетом доставки в нашу деревню. Если интересно, то связывайтесь с автором по ссылками ниже. Он продает и платы, и собранные девайсы. Проблем с покупкой никаких не было – быстро и четко, так что рекомендую.
Кстати, я все еще не оставляю надежды обзавестись “родным” старым РК, собранным “тогда”.
Когда я начинал проект эмулятора Радио-86РК на JavaScript написание, собственно, модели процессора Intel 8080 (КР580ВМ80А) было мучительной задачей. Конечно, в сравнении со современными процессорами старичок 8080 выглядит как Жигули первой модели перед новой БМВ. Но все равно, реализовать логику около двух сотен команд с десятком регистров и при этом нигде не налажать практически невозможно. Поэтому отладка выглядела так: делается изменение, затем пробуется парочка программ для РК (в основном игры), и если игра визуально не глючит, то значит все более менее нормально.
Так я привел эмулятор примерно в работающее состояние, и большинство программ для РК типа “работали”. Но, увы, после завершения отладки, мне больше не хотелось трогать код эмуляции процессора, так как перепроверка все была реально мучительной и более того ненадежной. Но по-хорошему, код эмуляция нуждался в доработках.
На следующем витке эмуляторного приступа, когда я решил запустить РК на Maximite, я разыскал доступные тесты для Intel 8080, написанные на его же собственном коде, и оформил их прямо в сборке стационарно. В итоге проект эмулятора процессора отпочковался в подпроект i8080-core. Это чистый эмулятор Intel 8080, непривязанный к конкретной аппаратуре. В процессе сборки запускаются четыре теста, один из которых, 8080-8085 CPU Exerciser, является, пожалуй, единственным способ убедиться в “похожести” эмулятора на реальный процессор. Он был адаптирован Вячеславом Славинским в процессе работы над эмулятором компьютера Вектор на FPGA.
Интересная особенность этого теста в том, что он не проверяет поведение процессора по документации, а просто проводит множество вычислений, задействуя как можно больше команд в как можно большем количестве комбинаций, считает контрольную сумму (CRC32) результат и сравнивает с эталонной. Эталонные значения были изначально получены прогоном теста на реальном процессоре.
С одной стороны данный тест не дает точного места, где происходит сбой, а просто говорит, например, что где-то в командах арифметики что-то работает не так, поэтому контрольная сумма не совпадает.
В другой стороны этот тест позволяет сравнить реальное поведение процессора, а не “документированное”, так как порой, как это водится, есть недокументированное или плохо документированное поведение.
Например, есть основные логические операции: OR, AND, XOR. Так как это неарифметические операции, то флаг половинчатого переноса AC (из младшей тетрады в старшую) просто обнуляется. Так написано в документации. Но в реальности команда AND особенная. В ней флаг AC устанавливается (внимание!) равным третьему биту операции OR между аргументами команды, а не просто в ноль. Данное поведение, все таки было задокументировано, но в более поздней документации по 8085.
Или, например, в командах инкремента и сложения, тот же флаг AC вычисляется как положено по документации. Но в командах декремента и вычитания этот флаг имеет инвертированное значение!
В общем, много интересного, “упущенного” в официальной документации.
В итоге я использовал “простые” тесты для начальной минимальной проверки, и затем CPU Exerciser для окончательной. Кстати, на реальном процессоре при частоте 2MHz этот тест работает около двух часов. Это не особо большая проблема для эмулятора на С, но для версии на JavaScript может стать проблемой.
Итак, после внедрения тестов я мог уже корячить код эмулятора i8080-core без особых проблем, играясь с оптимизацией, структурой и т.д.
И вот я решил вернуться к эмулятору на JavaScript и проверить его на тестах. Как я и предполагал, ни один из тестов полностью не проходил.
Я попробовал править существующую реализацию, но в процессе переписал код эмулятора 8080 практически заново, благо голова была свежа после работы над i8080-core. За пару дней я реализовал все команды и прикрутил тесты. По ним я выловил все глюки, и теперь i8080-js проходит все тесты, включая 8080-8085 CPU Exerciser, тем самым являясь реально точной репликой КР580ВМ80А.
В код эмулятора теперь разбит на компоненты (I8080, Memory, IO), что позволяет его легко использовать для эмуляции конкретной аппаратуры. Декодирование команд упрощено и теперь работает по принципу дизассемблера.
Тестирование можно проводить прямо в браузере через JavaScript Console, или с использованием интерпретаторов JavaScript V8 или SpiderMonkey, работающих из командной строки. Второй способ предпочтительнее, так как последний тест 8080-8085 CPU Exerciser на V8 работает около получаса, а на SpiderMonkey около трех часов.
Итак, код эмулятора i8080-js – http://github.com/begoon/i8080-js/
Идем далее, эмулятор Радио-86РК. Я его тоже фактически переписал с нуля, используя наработки от свежей версии на Maximite. Старая версия 0.6 все еще доступна, но новый эмулятор (версия 1.0 и выше) теперь будет хоститься на GitHub’е.
Код теперь также разбит на модули (UI, Screen, Keyboard, Memory, IO, Runner). Большая монолитная HTML-страница теперь состоит из нескольких скриптов и каталога с программами, которые загружаются динамически. Улучшена работа клавиатурой.
Попробовать эмулятор в деле: demin.ws/rk
Исходники на GitHub’e: http://github.com/begoon/rk86-js/
При отладке эмулятора был интересный эпизод, связанный, конечно, с тестированием. После отладки эмулятора процессора я был уверен, что в нем ошибок нет, ибо все тесты проходят. Более того, игровые программы, на которых я обычно провожу визуальное тестирование, работают. Но вот незадача – ни один из Бейсиков не работал. Все выводили на экран мусор и висли. Путем трассировки я выяснил, что команда RST была реализована неверно (адрес перехода вычислялся неправильно). Ни один из тестов эту команду почему-то не проверяет, игровые программы не используют, а вот Бейсики – практически все.
Итак, винтажный эмулятор Радио-86РК на JavaScript возвращается. Обновляйте закладки – demin.ws/rk
Впечатления и эмоции можно высказывать тут, ошибки и предложения лучше сразу файлить в трекер:
Посты по теме:
P.S. Не премяните заценить мой Сокобан (файл soroban.bin) и демку (файл rk86demo.bin).
P.P.S. Несколько скринов.
]]>
git diff --shortstat rel-2010 rel-2011
Выдает типа:
12 files changed, 2462 insertions(+), 488 deletions(-)
Если у вас проект не в git, то можно быстренько загрузить в него версии, требующие анализа.
]]>]]>
Знаю, что С - это начало всех начал, и при правильном использовании можно писать очень близко по эффективности к ассемблеру. Но, все же есть еще системы, где компилятору С сложно развернуться. Например, захотел я подыскать компилятор С для Intel 8080, чтобы замутить небанальную программу для Радио-86РК. Из реально собираемого я нашел только пару наследников знаменитого Small-C – smallc-85 и smallc-scc3.
Увы, для простейшей программы типа:
main() {
static char a;
for (a = 1; a < 10; ++a) {
++a;
}
}
Генерируется адъ типа:
;main() {
main:
; static char a;
dseg
?2: ds 1
cseg
; for (a = 1; a < 10; ++a) {
lxi h,?2
push h
lxi h,1
pop d
call ?pchar
?3:
lxi h,?2
call ?gchar
push h
lxi h,10
pop d
call ?lt
mov a,h
ora l
jnz ?5
jmp ?6
?4:
lxi h,?2
push h
call ?gchar
inx h
pop d
call ?pchar
jmp ?3
?5:
; ++a;
lxi h,?2
push h
call ?gchar
inx h
pop d
call ?pchar
; }
jmp ?4
?6:
;}
?1:
ret
Понятно, что много вопросов к компилятору, но в целом, Intel 8080 не очень удобен для компилятора С: деления/умножения нет, косвенной адресации через стек тоже нет и т.д.
Ладно, вернемся к Форту. В процессе обдумывания применения Форта для I8080 я написал удобный макро-ассемблер (но об этом будет отдельный пост) и попутно вспомнил об одном своем старом проекте времен Фидо: F-CODE. В качестве приема запутывания кода для защиты от отладчика я реализовывал мини-ядро Форта с прямым шитым кодом.
“Реализовывал мини-ядро”, конечно, звучит, круто, но в реальности интерпретатор шитого кода просто тривиален:
; F-Code Address Interpreter
GetNext$: cld
mov si, IP$
lodsw
mov IP$, si
retn
CALLR$: add RP$, 2
mov bp, RP$
mov ax, IP$
mov [bp], ax
pop word ptr IP$
next
RETR$: mov bp, RP$
mov ax, [bp]
mov IP$, ax
sub RP$, 2
next
NEXT$: call GetNext$
jmp ax
osPush$: call GetNext$
push ax
next
NEXT MACRO
jmp NEXT$
ENDM
Плюс несколько примитивов, реализованных также на ассемблере:
; Adc ( a b -> c isCarry )
; if a+b>FFFF isCarry = FFFF else isCarry=0
osAdc$: pop ax dx ; -> a b
add ax, dx
sbb dx, dx
push ax dx ; c isCarry ->
NEXT
; osSwap ( a b -> b a )
osSwap$: pop ax bx
push ax bx
NEXT
; osRot ( a b c -> b c a )
osRot$: pop ax bx cx
push bx ax cx
NEXT
osPut$: add RP$, 2
mov bp, RP$
pop word ptr [bp]
NEXT
osGet$: mov bp, RP$
push word ptr [bp]
sub RP$, 2
NEXT
osDrop$: add sp, 2
NEXT
; osNor ( a b -> a NOR b )
osNor$: pop ax bx
or ax, bx
not ax
push ax
NEXT
osTrap$: int 3
NEXT
; osPeek ( addr -> value )
osPeek$: pop bx
push word ptr [bx]
NEXT
; osPoke ( Value Addr -> )
osPoke$: pop bx ; -> Value Addr
pop word ptr [bx] ; ->
NEXT
И мы имеем полноценную стековую машину, на которой можно программировать. Конечно, когда начинаешь диассемблировать шитный код или трассировать, то надо думать, а иначе будут видны только бесконечные переходы туда-сюда. Желающие могут попробовать поковыряться в файле fcode.com. Правда, это досовский бинарь, и запускать его надо, например, под DOSBox. Программа предлагает угадать пароль.
Вот, например, код для вычисления CRC на данной стековой машине:
CalcCRC: CALLR ; ->
ofPush 0 ; CRC
ofPush 0 ; CRC 0
ofPeekb Buffer+1 ; CRC 0 Size
$For ; CRC
osI ; CRC i
ofPush Buffer+2 ; CRC i Buffer+2
osAdd ; CRC Addr
osPeekb ; CRC Value
osExch ; CRC Value*256
$For 0, 8 ; CRC Value
osShl ; CRC Value*2 isCarry
osRot ; Value*2 isCarry CRC
osSwap ; Value*2 CRC isCarry
osRcl ; Value*2 CRC*2 isCarry
$If <>0 ; Value*2 CRC*2
ofXor 8408h ; Value*2 CRC*2^Const
$Endif
osSwap ; CRC*2 Value*2
$Loop ; CRC Value*2
osDrop ; CRC
$Loop ; CRC
RETR
Красиво?
В процессе работы над F-CODE родился примитивный препроцессор для ассемблера, позволявший писать код типа:
lea dx, msg2
cmp bh, 3
$if <>0
lea dx, msg1
$else
hlt
$endif
cmp dx, 0C0DEh
$if =0
lea dx, msg2
$endif
mov cx, 2
$Do
$Do
cmp ax, 1
$EndDo =
dec cx
$EndDo Loop
Store ds, si, ax
$Do
cmp al, 1
$if <>0
$ExitDo
$endif
Store ax, bx, cx, es, bp
...
Restore
$ContDo
$EndDo Loop
Restore
Как и все утилиты во времена ДОС, препроцессор был написан на старом добром Турбо Паскале.
Понятно, что проект имеет чисто исторический интерес, хотя ничто не мешает реализовать интерпретатор Форта хоть на JavaScript’е, и использовать все уже готовые примитивы как есть.
Весь проект F-CODE лежит на GitHub’е – https://github.com/begoon/fcode. Для сборки нужны TASM/TLINK и Турбо Паскаль для препроцессора. Очевидно, что надо все делать в ДОСе.
P.S. При всей низкоуровневости, народ пишет на Форте весьма кучерявые программы. Например, nnbackup, написан на Форте.
]]>Основные моменты реализации:
А вот демка в действии.
Изучив ее вдоль и поперек, у меня возник план перенести ее на Радио-86РК. Напомню: 1.78MHz, 32Кб памяти, символьный экран 78x30 или 156x60 через псевдографику.
Для начала было интересно посмотреть, как могло бы выглядеть изображения на экране РК. Например, вот так (не могу понять почему, но надо нажать CTRL-R или F5 после перехода по ссылке, а иначе только черный экран).
В принципе, красиво, я решил продолжить. Взяв за основу исходник Вячеслава, я добавил туда вывод в стиле РК символами псевдографики - 178x60. Выглядит неплохо.
Итак, идея работает. Надо думать, как это все потенциально писать на 8-битном ассемблере Intel 8080, где даже деления нет. Надо переводить все в целые числа, а синус брать по таблице. Реализация с вещественными числами немного поменялась, стала использовать табличный синус. Несмотря на использование только 128 целых значений синуса на всем интервале от 0 до 2*PI, к тому же теперь домноженных на 256 и округленных в целое, выглядело тоже неплохо (случайно вышел забавный эффект в начале демки, когда буквы приезжают сзади).
Дальше надо было уже убирать библиотеку Processing.js, все переводить на целые и желательно положительные числа. Для целочисленности я все домножил на 256, а для положительности ввел смещение. Исходник уже становился все хуже и хуже, но все еще работал. Я везде по возможности упрощал константы и вставил проверки на положительность переменных.
И вот родилась первая версия на ассемблере, где можно было считать синус. В качестве эксперимента я попробовал рисовать синусоиду.
Очевидно, что с такой производительностью полный кадр 156x60 она будет считать полгода. Первый облом.
Решил я перейти в разрешение 78x30, просто символы, без псевдографики. Сам исходник на JavaScript несильно поменялся, разве что константы. Выглядело тоже сносно.
Уже эту версию я полностью реализовал на ассемблере 8080. Получился монструозный опус почти на тысячу строк. А работал он вот так… Второй облом.
Он отступать уж совсем поздно было, и я предпринял последнюю, хоть уже немного “нечистую” попытку. С помощью генератора на С я записал все кадры вместе с разницей между соседними. Сам же плеер этого самопального MPEG’а был очень короткий.
В итоге - ОНО! (на эмуляторе в браузере)
И теперь уже на настоящем РК (почти настоящем):
Уметь найти в себе силы признавать бесперспективность той или иной затей и вовремя остановиться.
Исходники все этого мучения доступны - https://github.com/begoon/rk86-3d-demo/
]]>Так как вопрос простой, то тут уж, как говорится, кто первый прочитал и догадался, тот и папа. Попробуйте ответить на вопрос самостоятельно, без гугла.
P.S. Кстати, угадывание моего motto хоть и закончено, но так как ответ не публикуется, у вас все еще шанс подумать.
]]>Легче, тоньше, воздушнее, быстрее.
Новый разъем, и наушники теперь снизу.
Новый разъем (папа).
Народ прилип к столам с айфоном.
Кстати, новые 3D-карты в iOS6 – супер! Увы, на мой iPhone 4 (не S), это не распространяется.
Ну и традиционная проверка на правильном сайте.
Пара слов.
Я хоть и сижу плотно на продуктах Apple, считаю себя все еще имеющим остатки здравого смысла. Если какие-то вещи от Apple неудачные или совсем говно (например, Magic Mouse), я радостно это признаю. Но братья! iPhone 5 – это реальное инженерное и дизайнерское чудо. Можно ненавидеть Apple за их пафос и понты, можно с ними судиться и даже выигрывать. Но! (например, Самсунг) Пожалуйста! Произведите что-то хотя бы близкое, а не ваши лопатные андроидофоны. И вы… займете место Apple!
]]>Итак, требования к библиотеке:
Нашлось два кандидата:
Обе библиотеки собрались без сучка и задоринки. miniz дает на моих данных 0.78 сжатия, minilzo - 0.71. Оба не дают вместить все в 512К.
В целом мне miniz понравился больше с точки зрения функциональности. К тому же эта библиотека вообще не требует дополнительной памяти для декомпрессии, а использует только выходной буфер. Для minilzo требуется статический буфер на 16Кб.
P.S. Попробовал я еще XZ Embedded (LZMA2). Там, вроде по экспериментам получалось сжать гораздо сильнее, но там требуется интерфейс malloc/free, поэтому сходу собрать под PIC32 не получилось.
]]>Внимание! Беспрецедентный случай – даю несколько тематических ссылок из своего нетехнического блога.
В Англии при коллективном посещении пабов есть культура так называемых раундов. Раунд - это когда один человек платит за напитки всей компании. Посещение паба группой - это череда раундов. Самый массивный - это, конечно, первый раунд. Каждый новый раунд обычно имеет нового владельца. Традиционно, когда ты допил и хочешь еще – ты встаешь и спрашиваешь кто и чего будет.
Вот тут наступает момент, когда народ тебе начинает называть напитки. У меня буфера хватает на 2-3 элемента, затем по схеме FIFO старые данные теряются. Поэтому возникла идея приложения, в котором можно было бы иметь список напитков, которые можно быстро объединять в группу.
Знаю, что есть мириады приложений, делающих что-то подобное. Но тут два момента: приложение должно быть максимально быстрым и максимально простым. К тому же мне хотелось написать ну хоть какой-то апп для айфона.
У меня родилось вот такое:
Просто и кондово, зато решает задачу на 100%. Лично сам регулярно пользуюсь. Увы, Apple Store “завернул” приложение как “не имеющее достаточно функциональности”. Ну и ладно.
В процессе написания пришлось освоить следующие классы:
Полностью собираемый проект лежит на GitHub’е - https://github.com/begoon/buyround. Предлагается как учебный материал.
Да, как уже писал в рассказе про свое первое приложение US Visa, для успеха мобильного приложения громадное значение имеет красивая иконка. Я “нашел” вот такую, хотя по-хорошему надо рисовать свои.
P.S. Сейчас вот вообще отойду от нормы и приложу картинку, на которой видно, в каких нечеловеческих условиях писался этот пост:
]]>
karateka.dsk.gz
и загрузить его в эмулятор AppleWin.
Есть досовская версия. Увы, в ней геймплей искажен. Например, в эпловской версии бойцы внутри дома сами не подходят, поэтому для атаки на них был прием “шажок вперед и два назад”. В досовской же версии можно просто мочить лоукиком, когда они приближаются.
Зато для досовской версии существует мой пачт, еще со времен Фидо:
Immortality
KARATEKA.EXE
00003066: 48 90
00003D7E: 48 90
Steel breast
KARATEKA.EXE
0000306E: 83 C6
0000306F: 3E 06
00003072: 3F 01
00003073: 7E 75
Flying kick for humans
KARATEKA.EXE
00002F30: 7E 00
Flying kick for bird
KARATEKA.EXE
00002E2F: 7F 00
00002E30: 01 00
00002E34: 3D 25
00002E35: 04 00
00002E44: 06 00
Kill bird by first kick
KARATEKA.EXE
000031BA: 85 33
Kill humans by first kick
KARATEKA.EXE
00002F3A: 85 33
С ним можно рубиться, например, так:
Зацените, как в конце принцесса нещадно, но тщетно мочит вас ногой.
P.S. Увы и ах, исходников этой игры вроде бы нет, а жаль. Непонятно мне, почему уже через столько нет нельзя их открыть.
]]>
Досовская реализация, сделанная какими-то энтузиастами и являющаяся очень точной репликой, прекрасно работает в DOSBox’е:
dosbox -exit BOLO.COM
Еще давно я сделал небольшой патч, чтобы узнать, что будет в конце игры:
Unlimited lives
BOLO.COM
00000174: 4F 00
00000178: DF 4A
00000179: FE FF
Unlimited fuel
BOLO.COM
00000A38: 06 04
Immortality & pass through the walls
BOLO.COM
00000C61: 01 00
0000172D: 01 00
00001817: 01 00
Теперь можно играть вот так:
Увы, исходников игры, видимо нет и не будет, поэтому остается только ее постепенно реверсить, благо, она написана на ассемблере, но все равно, перенос игры на, например, SDL, нетривиален. Я знаю, есть много переделок BOLO под Windows, но это все неоригинальные версии.
]]>В сети как-то немного информации, разве что небольшое интервью-рассказ. Но сейчас немного о другом. Многие группы создавали (и продолжают создавать) демо. Не все ж в дизассемблере сидеть, можно и по программировать иногда.
У UCL тоже было несколько демок. У меня несколько сохранилось. Прекрасно запускаются под dosbox’ом.
Вот видео одной, на мой взгляд самой красивой. Произведения человека под псевдонимом SkullC0Der.
Есть вопрос к спецам в компьютерной графике – объясните, пожалуйста, математическую модель преобразований для развивающихся флагом трех букв “UCL”?
Вроде каждый пиксель там как бы ездит по оси Z (уходящую внутрь экрана) вперед и назад, а по отношению к соседним точкам там какой-то гармонический закон. Но, увы, это только догадки, поэтому будут очень признателен совет эксперта.
]]>Знаю-знаю, зачем откапывать стюардессу, да еще и по второму разу и все такое, но все же.
Тут все по-взрослому. Уровни лежат запакованные, и распаковываются на лету.
Конечно, исходник писался и компилировался не на РК, а туда заливался только готовый бинарь. Это сильно упростило задачу. Хотя для полной аутентичности надо было писать и ассемблировать на самом же РК. Как-нибудь в другой раз.
Я, если честно, давно не писал на ассемблере что-то длиннее пары десятков строк, поэтому сначала было странное ощущение. Потом вспомнилось, что лучше в подпрограммах всегда сохранять регистры, не гнаться чрезмерной компактностью кода (по крайней мере сначала), и все пошло на лад.
Ассемблер, особенно старый, где нет разных свистелок типа деления, заставляет подумать о том, что реально требуется, и отбросить обобщения типа “мне это пригодится потом”. Например, подпрограмма вывода аккумулятора в десятичном виде. Так как значения могут быть только от 0 до 59 (00-3B), номер уровня, я решил сделать кондово: посчитать сколько раз удастся вычесть 10 – это будет первая цифра, а результат перед последним вычитанием будет второй цифрой.
print_dec: push psw push b mvi b, 0ffh print_dec_loop: inr b sui 10 jp print_dec_loop adi 10 push psw mvi a, '0' add b mov c, a call monitor_putchar pop psw adi '0' mov c, a call monitor_putchar pop b pop psw ret
Исходник лежит в составе проекта “Эмулятор Радио-86РК на Maximite”. На нем же я записал небольшой видос.
]]>Цена вопроса была 128€ + 25.80€ НДС. Удобно было, что у них есть склад в UK, поэтому доставка была бесплатной, быстрой и без таможни.
Изначально мой план был купить, поерзать немного и вернуть, ибо у них четко сказано, что в течение месяца можно вернуть без объяснения причины. Вот это, кстати, правильный и современный подход, и не будь этого условия, я бы может и не купил бы.
Оказалось, что это все на всего маленькая металлическая коробочка со спичечный коробок.
Комплектация
Подключаем…
В качестве теста я подцепил на последовательный порт Raspberry Pi, по которому в данный момент телнетом сидел в консоли.
Фрагмент захвата, где видно, что пролетает командочка ls
по одному
проводу и эхом летит назад по другому.
Теперь клавиатура GMC-4. Она сделана по классической схеме, когда по ножкам одного порта последовательно “летает” единичка, которая “выбирает” номер текущего ряда кнопок, а со второго порта просто считывается маска состояния кнопок выбранного ряда. Это позволяет значительно сократить количество отведенных под обслуживание клавиатуры ножек.
А так как я собирался имитировать клавиатуру, мне надо было понять временные параметры сканирующего сигнала. Подвесил я на анализ все четыре сканирующих выхода и вот:
Сразу видно, что к чему. При анализе можно измерять практически все параметры сигнала, менять масштаб, сравнивать с предыдущими замерами и т.д. На сайте есть видео в хорошем качестве, где все это подробно объясняется. У меня в электронике статус дилетанта, так что меня вставило от простоты анализа, но тот же товарищ Славинский говорит, что при анализе сложной цифровой схемы бывает крайне удобно, если можно захватывать не один, не два, а много сигналов одновременно.
Как вы уже поняли, возвращать девайс я не стал, хотя у меня была претензия к ним. У меня не получилось делать захват на максимальной частоте в 24MHz. Программа говорила, что девайс не успевает. Суппорт сказал, что может быть это связано с конкретно моим USB-хабом, но я разное перепробовал - проблема не исчезла. Они не отказывались ни разу взять девайс назад, но я его не отдал. Уж больно он мне понравился, и все мои текущие задачи он прекрасно решает.
Рекомендую.
]]>
Заглянув в исходники, я даже растрогался. Я сам себя удивил подробностью комментариев и обилием приятного, хотя и мучительного в поддержке, форматирования. Все ручками: графика, окна, мышка.
Для завершения путешествия в прошлое я даже записал два небольших видео.
И текстовая версия:
]]>comrade@gmail.com
. Письма, отправленные на адрес com.rade@gmail.com
также
придут на исходный адрес. Более того, аналогично для c.o.m.r.a.d.e@gmail.com
.
Но это мало полезно. Есть возможность интереснее. Письма, отправленные на:
comrade+fred@gmail.com
comrade+john@gmail.com
comrade+peter@gmail.com
…также будут доставлены на comrade@gmail.com
. Это удобно, так как можно будет
настроить фильтр на этом ящике, и в зависимости от суффикса после знака +
переадресовывать письма кому-то.
Получается эдакая легкая пародия на собственный почтовый домен на Gmail.
]]>Имея Mac и iPhone, не попытаться написать мобильное приложение? Как-то неправильно. Благо тут подвернулась задачка, которая прекрасно легла в тему, как весьма полезная и в то же время не очень сложная в реализации. Итак, я погрузился в Objective-C и Cocoa.
Прошу помнить, что не только мое первое приложение для iOS, но и первое приложение на Objective-C в принципе. Ни разу не претендую ни качество реализации, ни на эффективность, но хочу сказать, что получился весьма целостный несложный пример, который дает представление об Objective-C и разработке под iOS в целом. Особенно для тех, кто вообще этот язык не знает.
Данный пост был изначально опубликован в виде статьи в журнале «The Pragmatic Bookshelf Magazine» на английском языке – US Visa: My First iPhone App. Русская версия, публикуемая здесь, не является точным переводом журнальной версии, так как была написана как отдельный текст несколько позже.
За последний год я несколько раз вынужден был подавать на американскую визу в посольстве в Лондоне. Каждый раз мне говорили, что конкретно в моем случае требуется «administrative processing». Документы то у тебя принимают, но потом вместо визы дают номерок (batch number) и говорят периодически заглядывать на их сайт, где есть PDF-ка, в которой по данному номеру следует искать указания, что делать дальше (досылать еще документы, посылать паспорт и т.д.). Нажимаешь на ссылку, открывается файл, жмешь CTRL-F, вводишь номер (batch number) и вперед.
Возникла идея автоматизации – сделать приложение для айфона, в которое может вбить номер заявки один раз, и затем одним нажатием на кнопку получать статус обработки визы. Приложение должно уметь скачивать PDF файл, парсить его и вычленять данные по заявке.
Не все еще потеряно. Objective-C можно запустить на Windows через Cygwin или MinGW. Более того, проект GNUstep дает возможность использовать библиотеки AppKit и Foundation для написания графических программ в Windows на Objective-C. Увы, я не буду погружаться столь глубоко в этой статье. Мы сделаем только приложение, работающее в командной строке. Оно будет уметь скачивать PDF и парсить его. Собрать приложение можно будет и на Windows, и на Маке. После, мы практически без изменений будем использовать модули этого приложения для создания полноценной программы для iOS. Но, увы, это уже только для владельцев Маков. Можно, конечно, Хакинтош на виртуалку поставить и гонять приложение на симуляторе айфона в Xcode, но вот загрузить его в реальный айфон вряд ли получится без настоящего Мака.
Я нашел два великолепныx поста:
Я про Objective-C не знал ничего, кроме слухов о его необычном подходе к управлению памятью, поэтому пришлось пролистать следующие книжки.
Предупреждение: Ссылки снизу содержат мой личный номер партнерской программы с Амазоном. От возможных покупок, совершенных после перехода по этим ссылкам, я могу получить небольшой процент. Если вас это не устраивает, пожалуйста, не нажимайте на ссылки, или вручную «почистите» URL через cut-paste. Спасибо за понимание.
1. iOS Programming: The Big Nerd Ranch Guide, 3/e (Big Nerd Ranch Guides)
2. Objective-C Programming: The Big Nerd Ranch Guide (Big Nerd Ranch Guides)
3. Programming in Objective-C (4th Edition) (Developer’s Library)
А еще есть один волшебный бесплатный документ – «From C++ to Objective-C».
Итак, задача делится на три основные части:
После ознакомления с Objective-C, могу сказать, что для более менее опытного разработчика на C или C++, особенно, если есть опыт разработки UI (я в свое время много возился с Delphi/C++Builder), «въехать» в Objective-C и Cocoa несложно. Достаточно сфокусироваться на весьма необычной полу-ручной модели управления памятью (особенно после RAII в C++ и сборщика мусора в Java). Objective-C сам управляет памятью, но вот контроль за подсчетом ссылок на объекты для их правильного освобождения лежит на вас. Надо понять принцип, иначе утечки памяти неизбежны. У меня именно так и было в начале. Благо отличные инструменты профилировки в Xcode позволяют основные проблемы выявлять практически сразу.
Ниже я приведу несколько личных субъективных впечатлений, как новичка в Objective-C и Cocoa. Вряд ли это будет интересно, если вы уже имеете опыт в них, но вот если нет – думаю, будет интересно.
Для начала интересно посмотреть, как в Objective-C формируются имена функций-членов класса. Это почти как человеческий язык. Если я по-английски скажу «please, find a needle in a portion of some data and add the result to a list implemented as a mutable array», в Objective-C это будет:
+ (bool)findInPortion:(NSMutableData *)someData needle:(NSString*)aNeedle andAddTo:(NSMutableArray*)aList { ... }
Если прочитать этот код слева направо сверху вниз, то получается почти полноценное предложение. Формально, полное имя этого метода - findInPortion:needle:andAddTo:
. Аргументы именованы, и их имена являются частью полного имени метода. Если правильно давать имена переменных аргументов (someData
, aNeedle
and aList
), то можно фактически писать по-английски. Конечно, это все довольно «многословный» подход, но фантастическая система предсказания в Xcode при наборе кода позволяет быстро и просто набивать все эти обороты. Обратите внимание также, что традиционное выравнивание при разбивке длинных строк происходит по двоеточию, разделяющему формальное имя параметра от переменной, его представляющей.
В Objective-C нетрадиционный синтаксис для вызова методов. Например, вместо:
NSMutableArray* list = NSMutableArray.alloc.init;
пишется:
NSMutableArray* list = [[NSMutableArray alloc] init];
Выглядит странно, но это вопрос привычки. Опять таки, система предсказания кода при вводе позволяет вводить квадратные скобки даже почти физически не набивая их.
Objective-C и Cocoa используют активно несколько шаблонов программирования, которые просто необходимо освоить. Например, делегаты. Они везде в Cocoa. Делегат – это класс, содержащий в себе обратные вызовы. Вместе передачи пачки отдельных функций или методов, просто передается один объект, реализующий все требуемые обратные вызовы. Например, я использовал стандартный класс NSURLConnection для скачивания PDF’ки. Этот класс требует предоставление ему делегата NSURLConnectionDelegate, методы которого вызываются при различных событиях в процессе скачивания.
Итак, пара недель вечерних бдений за книгами, и я набросал остов моего первого приложения. Но это была только первая часть марлезонского балета. Далее надо было разобраться с форматом PDF.
Как уже было сказано, файл, содержащий информацию из посольства, в формате PDF. Описание этого формата доступно на сайте Adobe. Я использовал документ «PDF Reference third edition, Version 1.4».
Разбор PDF у меня реализован весьма кондово. Так как данные приходят порциями, то мы будем анализировать документ по частям, последовательно. Каждую новую порцию данных добавляем в буфер и пытаемся в нем разобрать формат PDF. Сначала ищем фрагменты, обрамленные в маркеры stream
и endstream
. Содержимое каждого такого блока «разжимаем» через zlib/inflate
. После это уже чистый текст, и мы в нем ищем наш batch number, конечно, с учетом языка разметки PDF. Если номер обнаружен, то печатаем его и переходим к следующему блоку.
Основные шаги парсера:
1. Если в данных, принятых на текущий момент, есть блок, ограниченных тегами stream\r\n
и endstream\r\n
, то вырезаем его из буфера и «разжимаем» через zlib/inflate
.
2. Разжатый на первом шаге блок являет текстовым. Нам надо найти в нем фрагменты, обрамленные тегами BT\r\n
(Begin Text) и ET\r\n
(End Text). Находим все такие блоки и объединяем их в список строк.
3. Внутри каждой строки, найденной на шаге 2, удаляем подстроки, неокруженные круглыми скобками. Все что вокруг круглых скобок – это служебная информация, и она нам не нужна.
4. Итак, мы вычленили чистый текст из PDF’ки. Логически информация в этом файле организована в виде таблицы с тремя колонками: номер заявки (batch number), статус и дата. Увы, среди этого еще попадаются колонтитулы страниц. Чтобы их отсеить, мы будем смотреть, что если текущая строка выглядит как batch number (11 цифр), то за ней обязательно идет строка-статус и строка-даты. Берем их и снова ждем нового batch number’а.
Как я уже сказал, разбор заточен под конкретный файл, и если в посольстве его изменят, то все сломается. Если хотя бы использовать регулярные выражения, то будет гораздо гибче, но я оставлю это читателям на самостоятельную проработку.
ДОПОЛНЕНИЕ. В процессе работы над статьей, появилась идея сделать специальный веб-сервис, обращаясь к которому по простым URL’ам можно получать данные о заявке, а вся «кухня» по разбору PDF’ки происходит «на облаке». В журнале Dr.Dobb’s недавно вышла моя статья - RESTful Web Service in Go Powered by the Google App Engine, описывающая данный подход. Желающие могут «допилить» приложение для работы через этот веб-сервис. Можно вообще сделать хитро: сначала обратиться к веб-сервису, и если от него есть ответ, то на этом закончить, а если нет – запустить процедуру самостоятельного скачивания и разбора PDF’ки.
Итак, мы знаем почти все, чтобы написать приложение, которое будет скачивать PDF и вычленять из него информацию по нашей заявке. Приложение будет работать из командной строки. Его можно будет собрать из на Маке, и на Windows через GNUstep и Clang. Далее, исходные файлы этого приложения будут использоваться без изменений для версии под iOS.
Файлы:
BatchPDFParser.m
(и .h
) – PDF-парсер.NSURLConnectionDirectDownload.m
(и .h
) – Скачивалка. Тут «обвеска» для NSURLConnection
(инициализация, делегаты, цикл обработки событий).DirectDownloadDelegate.m
(и .h
) – Делегат для NSURLConnection
, принимающий вызовы в различные моменты скачивания.ViewController.m
– прототип ViewController. Это «прослойка» между скачивалкой и будущим графическим интерфейсом. В OSX и iOS используется концепция MVC (Model-View-Controller). «Контроллер» обеспечивает связь между элементами интерфейса и бизнес-логикой приложения. Текущий контроллер в основном содержит заглушки, которые будут реализованы в полной графической версии.main-cli.m
– Точка входа.Этот файл содержит объявление класса Batch
, содержащего информацию об обновлении статуса заявки, и класса BatchPDFParser
, который реализует метод findInPortion:needle:andAddTo:
(кстати, это статический метод класса, видите +
начале строки?).
@interface Batch: NSObject { NSString *batchNumber, *status, *date; } @property (atomic, copy) NSString* batchNumber, *status, *date; @end @interface BatchPDFParser: NSObject + (bool)findInPortion:(NSMutableData *)data needle:(NSString* const)needle andAddTo:(NSMutableArray*)list; @end
В этом файле реализация парсера PDF.
#import <Foundation/Foundation.h> #import "BatchPDFParser.h" #import "zlib.h" @implementation Batch @synthesize batchNumber, status, date; - (void) dealloc { [batchNumber release]; [status release]; [date release]; [super dealloc]; } @end @implementation BatchPDFParser
Метод findInData:fromOffset:needle:
ищет подстроку в данном блоке данных (типа strstr()
). Поиск примитивный, и его можно ускорить, например, реализовав алгоритм КМП.
+ (int) findInData:(NSMutableData *)data fromOffset:(size_t)offset needle:(char const * const)needle { int const needleSize = strlen(needle); char const* const bytes = [data mutableBytes]; int const bytesLength = [data length] - needleSize; for (int i = 0; i < bytesLength;) { char const* const current = memchr(bytes + i, needle[0], bytesLength - i); if (current == NULL) return -1; if (memcmp(current, needle, needleSize) == 0) return current - bytes; i = current - bytes + 1; } return -1; }
Метод isBatchNumber:number:
проверяет, является ли строка номером заявки (batch number):
+ (bool) isBatchNumber:(NSString*)number { long long const value = [number longLongValue]; return value >= 20000000000L && value < 29000000000L; }
Метод findBatchNumberInChunk:needle:andAddTo:
ищет фрагменты, обрамленные тегами BT
и ET
. В них выделяет текст в круглых скобках, и уже среди найденного выделяет конкретно номер заявки, строку-статус и строку-дату.
+ (bool) findBatchNumberInChunk:(char const*)chunk needle:(NSString*)needle andAddTo:(NSMutableArray*)list { enum { waitBT, waitText, insideText } state = waitBT; enum { waitBatchNumber, waitStatus, waitDate } batchParserState = waitBatchNumber; NSMutableString* line = [[NSMutableString alloc] init]; Batch* batch = nil; bool found = NO; while (*chunk) { if (state == waitBT) { if (chunk[0] == 'B' && chunk[1] == 'T') { state = waitText; [line deleteCharactersInRange:NSMakeRange(0, [line length])]; } } else if (state == waitText) { if (chunk[0] == '(') { state = insideText; } else if (chunk[0] == 'E' && chunk[1] == 'T') { if (batchParserState == waitBatchNumber) { if ([self isBatchNumber:line]) { [batch autorelease]; batch = [[Batch alloc] init]; batch.batchNumber = line; batchParserState = waitStatus; } } else if (batchParserState == waitStatus) { batch.status = line; batchParserState = waitDate; } else if (batchParserState == waitDate) { batch.date = line; batchParserState = waitBatchNumber; if ([batch.batchNumber isEqualToString:needle]) { NSString* pair = [NSString stringWithFormat:@"%@\n%@", batch.status, batch.date]; [list addObject:pair]; NSLog(@"Found match: '%@' '%@' '%@'", batch.batchNumber, batch.status, batch.date); found = YES; } } [line autorelease]; line = [[NSMutableString alloc] init]; state = waitBT; } } else if (state == insideText) { if (chunk[0] == ')') { state = waitText; } else { char const c[2] = { chunk[0], 0 }; [line appendString:[NSString stringWithUTF8String:&c[0]]]; } } chunk += 1; } [line release]; [batch release]; return found; }
Теперь главный метод findInPortion:needle:andAddTo:
. Тут выделяются куски, обрамленные тегами stream\r\n
и endstream\r\n
, содержимое разжимается через zlib/inflate
и передается в findBatchNumberInChunk:needle:andAddTo:
на анализ.
+ (bool)findInPortion:(NSMutableData *)portion needle:(NSString*)needle andAddTo:(NSMutableArray*)list { static char const* const streamStartMarker = "stream\x0d\x0a"; static char const* const streamStopMarker = "endstream\x0d\x0a"; bool found = false; while (true) { int const beginPosition = [self findInData:portion fromOffset:0 needle:streamStartMarker]; if (beginPosition == -1) break; int const endPosition = [self findInData:portion fromOffset:beginPosition needle:streamStopMarker]; if (endPosition == -1) break; int const blockLength = endPosition + strlen(streamStopMarker) - beginPosition; char const* const zipped = [portion mutableBytes] + beginPosition + strlen(streamStartMarker); z_stream zstream; memset(&zstream, 0, sizeof(zstream)); int const zippedLength = blockLength - strlen(streamStartMarker) - strlen(streamStopMarker); zstream.avail_in = zippedLength; zstream.avail_out = zstream.avail_in * 10; zstream.next_in = (Bytef*)zipped; char* const unzipped = malloc(zstream.avail_out); zstream.next_out = (Bytef*)unzipped; int const zstatus = inflateInit(&zstream); if (zstatus == Z_OK) { int const inflateStatus = inflate(&zstream, Z_FINISH); if (inflateStatus >= 0) { found = found || [BatchPDFParser findBatchNumberInChunk:unzipped needle:needle andAddTo:list]; } else { NSLog(@"inflate() failed, error %d", inflateStatus); } } else { NSLog(@"Unable to initialize zlib, error %d", zstatus); } free(unzipped); inflateEnd(&zstream); int const cutLength = endPosition + strlen(streamStopMarker); [portion replaceBytesInRange:NSMakeRange(0, cutLength) withBytes:NULL length:0]; } return found; } @end
Заголовок делегата для NSURLConnectionDelegate
:
@protocol DirectDownloadViewDelegate<NSObject> - (void)setProgress: (float)progress; - (void)appendStatus: (NSString*)status; - (void)setCompleteDate: (NSString*)date; @end
Собственно, сам делегат NSURLConnectionDelegate
.
#import "DirectDownloadViewDelegate.h" @interface DirectDownloadDelegate : NSObject { NSError *error; BOOL done; BOOL found; NSMutableData *receivedData; float expectedBytes, receivedBytes; id<DirectDownloadViewDelegate> viewDelegate; NSString* needle; } - (id) initWithNeedle:(NSString*)aNeedle andViewDelegate:(id<DirectDownloadViewDelegate>)aViewDelegate; @property (atomic, readonly, getter=isDone) BOOL done; @property (atomic, readonly, getter=isFound) BOOL found; @property (atomic, readonly) NSError *error; @end
И его реализация:
#import <Foundation/Foundation.h> #import "DirectDownloadDelegate.h" #import "BatchPDFParser.h" @implementation DirectDownloadDelegate @synthesize error, done, found;
Конструктор initWithNeedle:andViewDelegate:
создает делегата и параметризирует его другим делегатом, DirectDownloadViewDelegate
, который будет использоваться для задачи обновления экрана. Тут, кстати, мы впервые видит и деструктор, (void) dealloc:
.
- (id) initWithNeedle:(NSString*)aNeedle andViewDelegate:(id<DirectDownloadViewDelegate>)aViewDelegate { viewDelegate = aViewDelegate; [viewDelegate retain]; needle = [[NSString alloc] initWithString:aNeedle]; receivedData = [[NSMutableData alloc] init]; expectedBytes = receivedBytes = 0.0; found = NO; return self; } - (void) dealloc { [error release]; [receivedData release]; [needle release]; [viewDelegate release]; [super dealloc]; }
Метод connectionDidFinishLoading:
вызывается, когда соединение закончено.
- (void) connectionDidFinishLoading:(NSURLConnection *)connection { done = YES; NSLog(@"Connection finished"); }
Метод connection:didFailWithError:
вызывает при ошибке при скачивании файла.
- (void) connection:(NSURLConnection *)connection didFailWithError:(NSError *)anError { error = [anError retain]; [self connectionDidFinishLoading:connection]; }
Метод connection:didReceiveData:
вызывается, когда получена новая порция данных из канала. Каждую такую порцию мы добавляем в буфер, обновляем индикатор прогресса скачивания (через еще один делегат, viewDelegate
), затем пробуем вычленить фрагменты данных по PDF формату, и, наконец, печатаем то, что было найдено.
- (void) connection:(NSURLConnection *)connection didReceiveData:(NSData *)someData { receivedBytes += [someData length]; [viewDelegate setProgress:(receivedBytes / expectedBytes)]; [receivedData appendData:someData]; NSMutableArray* list = [[NSMutableArray alloc] init]; bool foundInCurrentPortion = [BatchPDFParser findInPortion:receivedData needle:needle andAddTo:list]; for (id batch in list) { NSLog(@"[%@]", [batch stringByReplacingOccurrencesOfString:@"\n" withString:@"\\n"]); [viewDelegate appendStatus:batch]; } [list release]; found = found || foundInCurrentPortion; }
Последний обратный вызов делегата NSURLConnectionDelegate
, что мы используем, называется connection:didReceiveResponse:
. Он вызывается, когда получен информационный ответ по HTTP, содержащий заголовки. Мы из заголовка «Content-Length» берем длину будущего файла, чтобы позже сообразно обновлять индикатор скачивания.
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSHTTPURLResponse *)someResponse { NSDictionary *headers = [someResponse allHeaderFields]; NSLog(@"[didReceiveResponse] response headers: %@", headers); if (headers) { if ([headers objectForKey: @"Content-Length"]) { NSLog(@"Content-Length: %@", [headers objectForKey: @"Content-Length"]); expectedBytes = [[headers objectForKey: @"Content-Length"] floatValue]; } else { NSLog(@"No Content-Length header found"); } } }
В этом файле находится метод donwloadAtURL:searching:viewingOn:
, который мы добавляем к классу NSURLConnection
. Интересно тут то, что через понятие категорий в Objective-C можно «примешивать» новые методы к существующим классам. Тут мы к классу NSURLConnection
добавляем категорию DirectDownload
.
@interface NSURLConnection (DirectDownload) + (BOOL) downloadAtURL:(NSURL *)url searching:(NSString*)batchNumber viewingOn:(id)viewDelegate; @end
Ну и финальная часть скачивалки PDF. Метод donwloadAtURL:searching:viewingOn:
создает соединение и запускает скачивание. Затем происходит ожидание в цикле NSRunLoop
, пока скачивание не закончится. Этот цикл позволяет приложению реагировать на события в процессе скачивания. Обратите внимание, это до сих пор скачивалка ни как не привязана к графическому интерфейсу. Она использует делегат viewDelegate
для общения с «мордой» приложения.
#import <Foundation/Foundation.h> #import "DirectDownloadDelegate.h" @implementation NSURLConnection (DirectDownload) + (BOOL) downloadAtURL:(NSURL *)url searching:(NSString*)batchNumber viewingOn:(id)viewDelegate { NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url]; DirectDownloadDelegate *delegate = [[[DirectDownloadDelegate alloc] initWithNeedle:batchNumber andViewDelegate:viewDelegate] autorelease]; NSURLConnection *connection = [[NSURLConnection alloc] initWithRequest:request delegate:delegate]; [request release]; while ([delegate isDone] == NO) { [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:1.0]]; } if ([delegate isFound] != YES) { [viewDelegate appendStatus:@"This batch number is not found."]; NSLog(@"This batch number is not found."); } NSLog(@"PDF is processed"); [connection release]; NSDateFormatter* dateFormatter = [[NSDateFormatter alloc] init]; dateFormatter.dateFormat = @"yyyy/MM/dd HH:mm:ss"; NSString* lastUpdateDate = [dateFormatter stringFromDate:[NSDate date]]; NSLog(@"Last update at: %@", lastUpdateDate); [viewDelegate setCompleteDate:lastUpdateDate]; [dateFormatter release]; NSError *error = [delegate error]; if (error != nil) { NSLog(@"Download error: %@", error); return NO; } return YES; } @end
Как уже было сказано, в приложении для командной строки контроллер будет содержать только заглушки, которые мы реализуем позже в полной версии программы.
#import <Foundation/Foundation.h> #import "DirectDownloadViewDelegate.h" #define IBAction void
Пустой класс-заглушка ViewController
.
@interface ViewController : NSObject <DirectDownloadViewDelegate> @end #import "NSURLConnectionDirectDownload.h"
Адрес, откуда качать файл.
static char const* const pdf = "http://photos.state.gov/libraries/unitedkingdom/164203/cons-visa/admin_processing_dates.pdf";
И mock-реализация класса-контроллера.
@implementation ViewController
Тестовый обратный вызов appendStatus:
вызывается, когда обнаружено очередное обновление по заявке. Тут мы просто логируем, а в полном приложении будем обновлять экранную форму.
- (void) appendStatus:(NSString*)status { NSLog(@"appendStatus(): '%@'", [status stringByReplacingOccurrencesOfString:@"\n" withString:@"\\n"]); // Some code is skipped here because not required for the command line mode. }
Тестовый обратный вызов setProgress:
вызывается, когда после принятия очередной порции данных надо обновить индикатор скачивания.
- (void) setProgress:(float)progress { // Some code is skipped here because not required for the command line mode. }
Тестовый обратный вызов setCompleteDate:
вызывается, когда анализ PDF полностью закончен. Тут, опять, мы просто логируем.
- (void) setCompleteDate:(NSString*)date { NSLog(@"setCompleteDate(): '%@'", date); // Some code is skipped here because not required for the command line mode. }
Ну и финальный метод, который все запускает, updateBatchStatus:
. В полной программе он будет вызываться при нажатии кнопки на форме. Тут он вызывается из main()
.
- (bool) updateBatchStatus:(NSString*)batchNumber { NSURL *url = [[[NSURL alloc] initWithString:[NSString stringWithCString:pdf encoding:NSASCIIStringEncoding]] autorelease]; return [NSURLConnection downloadAtURL:url searching:batchNumber viewingOn:self]; } @end
Запуск из командной строки.
#import <Foundation/Foundation.h> #import "DirectDownloadDelegate.h" @interface ViewController : NSObject <DirectDownloadViewDelegate> - (bool) updateBatchStatus:(NSString*)batchNumber; @end int main(int argc, char *argv[]) { @autoreleasepool { ViewController* viewController = [ViewController alloc]; [viewController updateBatchStatus:[NSString stringWithCString:argv[1] encoding:NSASCIIStringEncoding]]; [viewController release]; } return 0; }
Makefile
для Мак:
files = \ ViewController.m \ BatchPDFParser.m \ NSURLConnectionDirectDownload.m \ DirectDownloadDelegate.m main-cli.m all: build run build: clang -o USVisaTest -DTESTING -framework Foundation -lz $(files) run: ./USVisaTest 20121456171
Makefile GNUmakefile
для GNUstep:
include $(GNUSTEP_MAKEFILES)/common.make TOOL_NAME = USVisa USVisa_OBJC_FILES = \ ../ViewController.m \ ../BatchPDFParser.m \ ../NSURLConnectionDirectDownload.m \ ../DirectDownloadDelegate.m \ ../main-cli.m USVisa_TOOL_LIBS = -lz ADDITIONAL_OBJCFLAGS = -DTESTING CC = clang include $(GNUSTEP_MAKEFILES)/tool.make run: ./obj/USVisa 20121456171
Набираем make
. Windows:
This is gnustep-make 2.6.2. Type 'mmake print-gnustep-make-help' for help.
Making all for tool USVisa...
Creating obj/USVisa.obj/../...
Compiling file ViewController.m ...
Compiling file BatchPDFParser.m ...
Compiling file NSURLConnectionDirectDownload.m ...
Compiling file DirectDownloadDelegate.m ...
Compiling file main-cli.m ...
Linking tool USVisa ...
Можно запустить проверить реальную заявку:
make run
У меня вывелось следующее:
This is gnustep-make 2.6.2. Type 'mmake print-gnustep-make-help' for help.
./obj/USVisa 20121456171
2012-06-19 17:27:11.472 USVisa[3420] [didReceiveResponse] response headers: {"Accept-Ranges" = bytes; "Cache-Control" = "max-age=600"; Connection = "keep-alive"; "Content-Length" = 2237242; "Content-Type" = "application/pdf"; Date = "Tue, 19 Jun 2012 16:27:11 GMT"; ETag = "\"4b2ca3e41de5ba4ae45670e776edfc3b:1339778351\""; "Last-Modified" = "Fri, 15 Jun 2012 16:06:15 GMT"; Server = Apache; }
2012-06-19 17:27:11.604 USVisa[3420] Content-Length: 2237242
2012-06-19 17:27:12.093 USVisa[3420] Found match: '20121456171' 'send passport & new travel itinerary' '14-Jun-12'
2012-06-19 17:27:12.104 USVisa[3420] [send passport & new travel itinerary\n14-Jun-12]
2012-06-19 17:27:12.111 USVisa[3420] appendStatus(): 'send passport & new travel itinerary\n14-Jun-12'
2012-06-19 17:27:13.769 USVisa[3420] Connection finished
2012-06-19 17:27:13.774 USVisa[3420] PDF is processed
2012-06-19 17:27:13.961 USVisa[3420] Last update at: 2012/06/19 16:27:13
2012-06-19 17:27:13.972 USVisa[3420] setCompleteDate(): '2012/06/19 16:27:13'
Итак, все работает: скачивалка и парсер PDF. Теперь займемся версией под iOS. Увы, только для пользователей Mac.
Я сделал приложение крайне простым: одна форма с полем ввода, кнопкой и местом для вывода обновлений.
Индикатор скачивания и крутящийся бегунок появляются временно.
Вот сейчас это полная реализации контроллера. Через макрос TESTING
я сделал разделение между упрощенной и полной версией.
#import <Foundation/Foundation.h> #import "DirectDownloadViewDelegate.h" #ifdef TESTING #define IBAction void @interface ViewController : NSObject <DirectDownloadViewDelegate> @end #else #import "ViewController.h" #endif #import "NSURLConnectionDirectDownload.h" static char const* const pdf = "http://photos.state.gov/libraries/unitedkingdom/164203/cons-visa/admin_processing_dates.pdf"; @implementation ViewController #ifndef TESTING @synthesize updateProgressView, batchNumberTextField, statusTextView, lastUpdatedLabel, updateButton; #endif NSString* const PropertiesFilename = @"Properties"; NSString *pathInDocumentDirectory(NSString *fileName) { NSArray *documentDirectories = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES); NSString *documentDirectory = [documentDirectories objectAtIndex:0]; return [documentDirectory stringByAppendingPathComponent:fileName]; }
Сейчас обратный вызов appendStatus:
не только логирует, но и обновляет экранную форму.
- (void) appendStatus:(NSString*)status { NSLog(@"appendStatus(): '%@'", [status stringByReplacingOccurrencesOfString:@"\n" withString:@"\\n"]); #ifndef TESTING if ([[statusTextView text] length] == 0) [statusTextView setText:@"Status:\n"]; [statusTextView setText:[[statusTextView text] stringByAppendingString:status]]; [statusTextView setText:[[statusTextView text] stringByAppendingString:@"\n"]]; #endif }
setProcess:
обновляет индикатор скачивания.
- (void) setProgress:(float)progress { #ifndef TESTING updateProgressView.progress = progress; #endif }
setCompleteDate:
выводит дату обновления в текстовое поле на экране.
- (void) setCompleteDate:(NSString*)date { NSLog(@"setCompleteDate(): '%@'", date); #ifndef TESTING [lastUpdatedLabel setText:date]; #endif } - (bool) updateBatchStatus:(NSString*)batchNumber { NSURL *url = [[[NSURL alloc] initWithString:[NSString stringWithCString:pdf encoding:NSASCIIStringEncoding]] autorelease]; return [NSURLConnection downloadAtURL:url searching:batchNumber viewingOn:self]; }
Теперь несколько вызовов, специфичных для iOS. Метод viewDidLoad:
вызывается системой, когда экранная форма загружена и готова к использованию. Тут мы вручную создаем крутящийся бегунок и подправляем высоты двух элементов, кнопки и поля ввода, так как почему-то Xcode Interface Builder не позволяет менять их при дизайне формы.
#ifndef TESTING - (void)viewDidLoad { [super viewDidLoad]; // Do any additional setup after loading the view, typically from a nib. spinnerActivityIndicatorView = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleWhiteLarge]; [spinnerActivityIndicatorView setColor:[UIColor blueColor]]; CGSize size = [[self view] frame].size; [spinnerActivityIndicatorView setCenter:CGPointMake(size.width / 2, size.height / 2 + 60)]; [self.view addSubview:spinnerActivityIndicatorView]; CGRect rect = [self.updateButton bounds]; rect.size.height += 10; [self.updateButton setBounds:rect]; rect = [self.batchNumberTextField bounds]; rect.size.height += 20; [self.batchNumberTextField setBounds:rect]; #ifdef DEBUG NSLog(@"DEBUG mode"); #endif }
viewDidUnload
вызывается, когда форма становится неактивной.
- (void)viewDidUnload { [super viewDidUnload]; // Release any retained subviews of the main view. }
Метод shouldAutorotateToInterfaceOrientation:
позволяет контролировать поведение для смене ориентации аппарата. Тут мы разрешаем только портретное положение, не вверх ногами.
- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation { return (interfaceOrientation == UIInterfaceOrientationPortrait); } #endif
Метод launchUpdate:
вызывает при нажатии на кнопку Update
на форме. Мы блокируем кнопку от повторного нажатия, вывод индикатор скачивания и крутящийся бегунок.
- (IBAction)launchUpdate:(id)sender { [self setProgress:0.0]; #ifndef TESTING [updateButton setEnabled: NO]; [updateProgressView setHidden:NO]; NSString* previousStatus = [statusTextView text]; [statusTextView setText:@""]; NSString* batchNumber = [batchNumberTextField text]; [spinnerActivityIndicatorView startAnimating]; BOOL const ok = [self updateBatchStatus:batchNumber]; [spinnerActivityIndicatorView stopAnimating]; if (!ok) { UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"Error" message:@"Internet connectivity problem" delegate:self cancelButtonTitle:nil otherButtonTitles:@"OK", nil]; [alert show]; [alert release]; [statusTextView setText:previousStatus]; } [updateProgressView setHidden:YES]; [updateButton setEnabled: YES]; #endif }
Методы saveProperties:
и loadProperties:
сохраняют и восстанавливают содержимое формы при запуске и остановке приложения. Обратите внимание, что для сохранения данных в файле надо запросить у системы положение предназначенного для этого каталога.
- (void) saveProperties { NSDictionary *props = [[NSDictionary alloc] initWithObjectsAndKeys: #ifndef TESTING batchNumberTextField.text, @"batchNumberTextField", statusTextView.text, @"statusTextView", lastUpdatedLabel.text, @"lastUpdatedLabel", #endif nil]; for (NSString* key in props) { NSLog(@"%@ - %@", key, [props objectForKey:key]); } NSString* filename = pathInDocumentDirectory(PropertiesFilename); if ([props writeToFile:filename atomically:YES] == NO) NSLog(@"Unable to save properties into file [%@]", filename); [props release]; } - (void) loadProperties { NSDictionary *props = [[NSDictionary alloc] initWithContentsOfFile:pathInDocumentDirectory(PropertiesFilename)]; for (NSString* key in props) { NSLog(@"%@ - %@", key, [props objectForKey:key]); } #ifndef TESTING [batchNumberTextField setText:[props objectForKey:@"batchNumberTextField"]]; [statusTextView setText:[props objectForKey:@"statusTextView"]]; [lastUpdatedLabel setText:[props objectForKey:@"lastUpdatedLabel"]]; #endif [props release]; } - (IBAction)textFieldReturn:(id)sender { #ifndef TESTING [sender resignFirstResponder]; #endif } -(IBAction)backgroundTouched:(id)sender { #ifndef TESTING [batchNumberTextField resignFirstResponder]; #endif } @end
Все! Мы рассмотрели все основные файлы. Приложение полностью готово. Можно собирать и заливать в аппарат (не забыв купить у Apple лицензию разработчика).
Я выложил полный проект на GitHub – usvisa-app. Замечания и мысли принимаются.
Можно заценить видео:
https://www.youtube.com/watch?v=fxKlXDsDANU
Если вы подумываете о том, чтобы ваше приложение было распродано миллионным тиражом, стоит начать с красивой иконки. Для приложения обычно надо их несколько: 57x57 и 114x114 для непосредственно приложения, и 512x512 и 1024x1024 для публикации в AppStore.
Мы поступим проще и возьмем иконку из открытых источников – The Great Seal of the United States.
Я решил написать пост про это приложение после того, как цензоры AppStore его «завернули», сославшись на пункт в правилах, где говорится, что приложения с минимальной функциональной нагрузкой, которые можно реализовать через HTML5, не будут допущены. Видимо, они более не хотят видеть пукающих или просто отображающих статическую картинку приложений. Можно было бы поспорить с цензорами на тему минимальной функциональной нагрузки или реализации через HTML5, но я забил. Во-первых, лично мне нравится, что Apple старается не пропускать бесполезные и некачественные приложения, и во-вторых – я и так получил массу удовольствия от освоения Objective-C, и на данный момент работаю еще над двумя приложениями.
Скоро будет еще статейка про разработку приложений для iOS для новичков, так что следите за анонсами.
]]>Сразу скажу, что никакого приза за решение этой задачки не полагается, кроме почета и ссылки, которую я могу поставить вместе с именем решившего.
Просьба не постить решение нигде в комментарии, чтобы не портить другим возможность решить самостоятельно.
]]>
Это был простой уровень, но как насчет вот такого?
Элементарно запускается через DOSEmu: DOSEmu -exit pusher.exe
.
Кому лень запускать можно посмотреть демку:
И стало мне интересно, как шестьдесят уровней помещаются в столь небольшой
бинарь. Поковырявшись немного IDA’ой, я написал программку, которая выдирает
из бинаря pusher.exe
все уровни и печатает их в текстовом виде:
*************************************
Maze: 1
File offset: 148C, DS:00FC, table offset: 0000
Size X: 22
Size Y: 11
End: 14BD
Length: 50
XXXXX
X X
X* X
XXX *XXX
X * * X
XXX X XXX X XXXXXX
X X XXX XXXXXXX ..X
X * * ..X
XXXXX XXXX X@XXXX ..X
X XXX XXXXXX
XXXXXXXX
*************************************
Можно скачать сразу все уровни.
Уровни сжаты чем-то вроде Хаффмана – битовыми цепочками переменной длины. Каждый уровень кодируется следующим образом:
1 D3 D2 D1
, и тогда количество повторений
рассчитывается по формуле N = 2 + D3*4 + D2*2 + D1
, то есть от 2-х до
9-и символов. КОД СИМВОЛА имеет пять разных значений: 00 - пустое место,
01 - стена, 10 - бочка, 110 - место для бочки, 111 - бочка, уже стоящая
на месте.И так все 60 уровней.
В файле pushermaps.c можно увидеть весь нехитрый декомпрессор.
В процессе дизассемблирования образовались уровни в удобном текстовом, но все еще сжатом виде, например:
level_01 db 16h, 0Bh, 0A2h, 0DFh, 38h, 32h, 1Fh, 38h, 2Ah, 3, 0E6h
db 12h, 0C0h, 0A5h, 0F2h, 83h, 2, 81h, 3, 0E4h, 12h, 82h
db 25h, 6, 0CDh, 64h, 22h, 51h, 0ACh, 11h, 0A1h, 0Ah, 5
db 0E5h, 11h, 0B1h, 14h, 82h, 29h, 82h, 31h, 0A0h, 0E1h
db 2Ch, 18h, 0D1h, 0CFh, 80h, 0Ch, 8
level_02 db 0Eh, 0Ah, 0F6h, 58h, 0Ch, 68h, 0Dh, 94h, 0C6h, 80h
db 85h, 2, 82h, 18h, 0D0h, 15h, 4Ch, 10h, 0C6h, 0C2h, 18h
db 21h, 8Dh, 1, 6, 4, 39h, 10h, 0A0h, 81h, 80h, 85h, 2
db 8, 20h, 60h, 34h, 1Bh, 0Ch, 1Eh, 0CAh, 7, 4
level_03 db 11h, 0Ah, 0E3h, 9Fh, 0Eh, 7, 0C2h, 11h, 42h, 1Fh, 8
db 50h, 23h, 0E0h, 85h, 4, 0Ch, 1Eh, 84h, 8, 0A6h, 0B4h
db 10h, 85h, 2, 82h, 59h, 0D4h, 28h, 14h, 90h, 0D6h, 83h
db 0DFh, 7Ch, 0Eh, 1
И т.д.
Так что если захочется где-нибудь забацать свой простенький и компактный Сокобан, какое-то количество уровней можно взять готовыми.
Знаю, что в интернете полно уровней для Сокобана, и автоматические решалки имеются, но это совершенно не отменяет фан копания дизассемблером в бинаре более чем двадцати летней давности.
Проект на GitHub’e – https://github.com/begoon/sokoban-maps, если кому интересно.
]]>(фото из Википедии)
А тут – родной!
Конечно, надо было открыть. Шлейфы отсоединять я побоялся, поэтому фотографировал сбоку с двух ракурсов.
Справа внизу видна надпись “1982 ISSUE TWO”.
Совершенно очаровательно навешаны странные элементы прямо на корпусах микросхем. Может “доводили” при наладке, а может бывший владелец лазил.
В комплекте товарищ надавал родных кассет. Лично я до этого никогда не видел кассет по 15 минут.
“Мафон”, увесистый блок питания и джойстик.
Очаровательные книжечки.
Не стеснялись раньше давать правильные фотографии в руководствах.
Купил для такого случая на блошином рынке старый телек за фунт (50р).
Увы, этот Спектрум так и не завелся. При включении пищал и выводил мусор. Так как я не боец в такого рода починке, пришлось все вернуть. Продавец хоть и понудел что-то там на тему проблем с синхронизацией видеоизображения, но после просмотра видео (ниже) согласился, и я, скрепя сердце, отослал все назад. Остались только фотографии для поста.
]]>
Предупреждение: Данная статья является переводом с английского. Я не профессиональный переводчик, поэтому в тексте могут встречаться мелкие неточности. Желающие всегда могут прочитать оригинал на английском.
Программы должны быть в форме понятных комментариев, объясняющих назначение кода, который следует за этими комментариями. Форматирование должно быть таковым, чтобы читателю было легко и просто понять ваш код. А компилятору без разницы. В частности, следуйте соглашениям, принятым в математике и вашем естественном языке, а не вычитанным в каком-то непонятном руководстве по языку программирования. Сначала пишите комментарии, а затем код, и не наоборот. Если вы не знаете точно, что хотите получить и почему, любой код, который вы напишете, будет по определению, неверным.
«Этюды» не похожи на другие учебники по программированию. В них есть задачи из наиболее важных областей, и преподнесены они весьма нетрадиционным образом. Как родилась идея собрать реалистичные задачи и обыграть их в виде законченных игровых ситуаций? Можете рассказать вкратце как родились «Этюды»?
С 1974 по 1979 я преподавал в университете UC Davis. Там была новая группа выпускников по специальности «Информатика», призванных составить учебную программу по этому предмету, но в реальности это была просто группа людей из разных факультетов. Департамент прикладных наук нанял меня как первого профессора по этой специальности для создания учебной программы. Вместе с небольшой группой других профессоров и преподавателей мы создали новый учебный план с учетом требований по специальности и начали преподавать новые и переработанные курсы.
Работая над учебной программой, мы сошлись во мнении, что студентам стоит иметь опыт работы над реальными задачами в дополнение к теоретическим знаниям. Но под «реальными» задачами я не имею в виду коммерческие или прикладные задачи, так как я убежден, что такие знания очень быстро устаревают. Вместо этого я хотел дать студентам задачи, в которых надо было понять требования и поработать над спецификациями. Кроме этого, необходимо было научить студентов работать в группе. Например, задача по созданию компилятора давалась группе студентов из 2-3 человек, и оценка ставилась всей группе. Также надо было описать, как они поделили работу, и был ли выбор удачным.
Через некоторое время мы решили начать курс программирования проектов, чтобы закрепить знания студентов по созданию законченных приложений. Я сформулировал несколько проектов, которые мы придумали. Но, когда потребовались еще идеи, вокруг были только книги с примитивными задачами, например, распечатать таблицу истинности булевых функций и пару задач, используемых в университете Карнеги-Меллон. Так, сам того не замечая, я уже писал «Этюды». Так вышло, что я ушел из университета UC Davis практически сразу после публикации книги и точно не знаю, продолжили ли они использовать книгу в курсе по проектам.
Сколько длилась работа над книгой?
Я работал над «Этюдами» полтора-два года. Вначале уже было несколько готовых глав от курсовых. Договор с издательством подписали очень быстро, как я помню, где-то за месяц. Затем около года я писал остальные главы. Конечно, приходилось это делать вместе с основной работой. Рецензирование и корректура заняли еще месяцев шесть. И через пару месяцев после окончания работы я получил первую копию.
Почему вы не писали книг после «Этюдов», и почему «Этюды» не переиздавались?
Из университета UC Davis я ушел работать в Bell Labs. Там я вел проект по созданию первых компиляторов языка Ада. Мы публиковали техническое отчеты, но писать книгу реально не было времени. После Bell Labs я переехал в Силиконовую Долину работать в стартапе, и опять для книги не было времени. Далее появилась семья, а дети, как большинство родителей знает, требуют времени. Ну, а дальше вы и сами знаете.
В семидесятых Юрис Хартманис, один из моих любимых учителей, сказал мне, что если написать учебник по информатике, то должно хватить денег на новую машину. В те времена было много библиотек, которые все были готовы купить хотя бы по одной копии, обеспечивая минимальный уровень дохода хотя бы на Фольксваген (Фольксваген Жук тогда еще выпускался!). Если повезет, то могло хватить и на Мерседес. Естественно, если написать что-то вроде «Экономикс» Самуэльсона, то можно ежегодно покупать Ламборгини.
Но успех учебникам приходит, только когда они используются в учебных курсах. Увы, только несколько колледжей в Штатах начали использовать «Этюды», поэтому денег это приносило немного. Издательство не горело желанием работать над переизданием. Книга хорошо продавалась индивидуальным покупателям, но для учебников это - несущественный бизнес.
Но кое-что необычное произошло. Сразу после публикации «Этюдов» советское министерство образования посмотрело книгу и решило купить права на переиздание на территории Советского Союза. Если честно, это был настоящий дипломатический прорыв. Обычно Советский Союз просто игнорировал авторские права. Данный платеж был приятным сюрпризом.
Увы, в абсолютном эквиваленте сделка была не очень прибыльной. Они заплатили за права 1200 долларов США, что даже в семидесятых было не очень много. Я получил половину, $600. Конечно, было здорово, что «Этюды» были переведены на русский, после чего весьма широко использованы в советской учебной программе. Например, я работал с двумя русскими, использовавшими книгу, будучи студентами. Александр, вы третий! В целом, было напечатано около 60-ти тысяч экземпляров на русском языке.
И какую же машину с этого мне было купить? В итоге, я купил новое пианино, которое до сих пор стоит у меня в гостиной.
Как вы находили темы для этюдов?
По-разному. Пара задач были просто взяты из жизни. Например, задачи для этюдов про расчет расхода бензина и дохода от инвестирования я использовал сам. Часть задач родилась из моего увлечения компьютерными играми и искусственным интеллектом, например, про игры Калах и Менеджмент. Задачи про языки программирования в конце книги были вариантами из нашего курса по компиляторам. Какие-то этюды были классическими теоретическими задачами, просто немного завуалированными: машины Тьюринга, раскрашивание карты, сканер форматов.
В четвертой главе задача про автоматическое форматирование текста придумана моим коллегой из Ливерморской лаборатории. Он создал один из самых первых форматировщиков текста, который я использовал, работая над диссертацией и над черновиками «Этюдов». Не стоит забывать, что это было до того, как UNIX стал популярным, и даже программа nroff был чем-то «продвинутым». Поэтому задача, поставленная в этой главе, выглядела актуальной.
Приведенные в книге решения выбраны среди тех, что «умещались» в физический размер книги. Я также хотел показать студентам реальный код. Помните, в последней задаче даже говорилось, что решение является неполным.
Для каждого этюда вы рекомендовали время исполнения. С 1978 года языки программирования «немного» изменились, став «чуть более» развитыми. Но даже сейчас ваши рекомендации выглядят весьма жестко. Вы пробовали «проиграть» этюды, работая над книгой? Есть ли у вас любимый этюд?
Сейчас понятно, что это было ошибкой. Даже по нынешним меркам, даже на таком языке как Руби (кстати, моем любимом на сегодняшний день), который так много делает за вас, рекомендованные длительности исполнения остаются весьма суровыми. Не стоит забывать, что когда «Этюды» создавались, UNIX только набирал популярность, Бейсик, Фортран и Паскаль были основными языками обучения, а C только появился. Фактически не было интерактивных терминалов для программирования, и студенты готовили программы на перфокартах (хотя у моих студентов был доступ к терминалам, благодаря связям в Ливерморской лаборатории).
Сложно сказать, какой этюд у меня любимый. Мне нравится интерпретатор языка TRAC, так как приходится возвращаться к основам языков программирования, а эти вопросы часто упускаются в учебной программе. Языки TRAC и макрогенератор Strachey были придуманы в одно и то же время, независимо друг от друга. Оба автора опубликовали свои работы с интервалом в несколько месяцев. Языки очень похожи и демонстрируют, как работа с текстовыми данными может решать сложные задачи. Очень полезно изучать их вместе, чтобы увидеть, в чем эти языки отличаются, а в чем, напротив, похожи. Еще мне нравится этюд про перемещающий загрузчик, так как он решает все еще актуальные задачи в трансляции языков программирования. Мне до сих пор непонятно, почему подобные приемы все еще не используются сплошь и рядом.
Если честно, у меня не было достаточно времени прорешать все задачи в процессе работы над книгой. Однако, работая в Bell Labs, я был вынужден изучать UNIX и ее утилиты. Так, например, я решил реализовать этюд про расход бензина на awk’е. Забавно, я нашел ошибку в самом awk’е и отписал об этом Брайну Кернигану. Он сказал, что знал об этой проблеме, но никому не удавалось ее локализовать. Ошибка была классической – использование памяти после ее освобождения.
Не могу не спросить про этюд о шифре Виженера. Там была парочка «подстав», например «незначительная» опечатка в виде пропущенной строки в середине зашифрованного текста и также «немного неверный» метод взлома, предлагаемый как вариант решения. Конечно, ничто из этого не остановило читателей от успешного взлома, и даже сделало этюд еще более интересным. Все же, что там произошло с этой главой?
Да, неудобно вышло. При подготовке текста книги в издательстве все черновики перенабирались заново, но вот картинки печатались с оригинала как есть. Увы, моя картинка с зашифрованным текстом содержала ошибку. Интересно, что до перевода на русский никто не жаловался (или по крайней мере мне не сообщали). А неточности в самой задаче были скорее всего из-за моего изложения. Хотелось бы надеяться, что это единственное подобное место в книге.
Мне кажется, что подготовка и возможно прорешивание задач для книги в целом было задачей не из простых. Есть ли еще подобные сюрпризы в книге?
Задуманных сюрпризов нет. Когда я решал задачу про расход бензина, я осознал, что постановка недостаточно четкая. Возможно, было еще несколько мест, где могли требоваться уточнения.
Думаю, в целом получилось не так уж и плохо. Конечно, подготовка решабельных задач заняла гораздо больше времени, чем требовалось студентам на их решение. Мне не хотелось глупо выглядеть перед читателями.
К счастью, мне помогали, чтобы было очень кстати. Текст и картинки были изначально подготовлены с использованием новых средств форматирования и рисования, разработанных Хэнком Моллом и Джоном Битти. Это очень упрощало работу. Я писал сам текст на «формулярах», специальных листах для перфокарт. Эти листы затем отправлялись людям в группу «набивки», которые заодно еще и отчитывали текст. Если им казалось, что я допустил ошибку (или явную несуразицу), они набивали оба варианта, исходный и исправленный, чтобы я мог выбрать правильный.
Издатель попросил нескольких сторонних людей прочитать книгу и высказать свое мнение. Один из них оставил комментарии фактически на каждой строке текста. Именно он помог сделать книгу значительно лучше. Тогда я не знал, кто именно это был. Позже обнаружилось, что это был Стив Мачник (известный своей книгой про оптимизацию в компиляторах). Я был ему очень благодарен.
Если бы вы писали Этюды сегодня, что бы вы добавили?
Для начала, я определенно хотел бы исправить ошибки в задаче про шифр.
Электронная подпись и криптография с открытым ключом могли бы стать новой задачей. Также я бы подправил главу про арифметику высокой точности. Однозначно я бы изменил главу по языкам программирования и компиляторам с учетом современных подходов. Еще идея (изначально от Джона Флетчера) – сделать пару этюдов по написанию интерпретатора Лиспа на Руби (например) и упрощенный Руби на Лиспе. Или что-то в этом роде.
Я бы, возможно, убрал один или два простых этюда. Есть много задач из области искусственного интеллекта (в частности, генетического программирования), которые пришлись бы в тему. Можно было взять что-нибудь из нового в области структур данных (например, насколько красно-черные деревья производительнее простых двоичных деревьев). Если честно, сложно однозначно сказать, какая б задача точно подошла, до тех пор, пока не поработаешь над ней.
Большинство этюдов сфокусированы на реализации, нежели на том, как в целом подступиться к задаче, так как почти всегда вы даете подробное описание метода решения. Как вы думаете, нужно ли также давать студентам задачи без какой-либо подсказки о возможном решении? Что это могли бы быть за задачи? И как следовало бы их оценивать по-вашему?
В жизни всегда встречаются проблемы, в которых сначала надо понять, в чем именно проблема. Было бы неплохо обучить студентов подобному анализу, так как в реальности подобный навык – редкость. Но это не было изначальной идеей «Этюдов», так как это тема достойна целого отдельного курса. В «Этюдах» предполагалось давать студентам осмысленную задачу для практики полного цикла разработки. Здесь все еще очень много того, что студент должен самостоятельно решить. Например, решения оценивались по качеству кода, по умению автора объяснить результаты и по производительности программы.
Тут возможно сделать целый курс из задач того типа, что вы предлагаете, но тогда на лекциях надо разбирать методы подобного анализа, и книга также должна включать в себя соответствующие материалы. Это получится совсем другая книга.
Чарльз, в плане ваших интересов и работы вне «Этюдов». Я знаю, вы эксперт в области компиляторов. Вы работали, например, над созданием компилятора языка XPL. Над чем вы сейчас работаете в Оракле? Все еще программируете?
Да, программирую. Что мне нравится в программировании, так это то, что можно реально сделать то, о чем думаешь.
Моя основная область знаний – это теория, разработка и реализация языков программирования. Когда я был студентом, информатика как таковая еще не существовала, и моей специальностью была прикладная математика. Но мой диплом был связан с добавлением новых возможностей в язык TRAC (не напоминает один из этюдов?). Я работал над XPL, над полной компилирующей средой для Ада, над LR-генератором парсеров грамматик (вместе в Элом Шэнноном), над предложениями по добавлению массивов в Фортран, над новым языком описания потоков данных (вместе с Джимом МакГроу), и (довольно недавно) над новым генератором кода и оптимизатором для ораклового языка PL/SQL. Последнее время я работаю над некоторыми специальными проектами Оракле в области языков программирования. Если все получится, вы сможете прочитать об этом.
Глядя на тематику задачи в вашей книге, трудно угадать вашу личную область интересов. Даже в разделе по компиляторам вы даете задачу про интерактивный функциональный язык TRAC наряду с классическим процедурным языком. Так какие же области информатики вас интересуют больше всего?
Больше всего меня интересуют:
Языки программирования, в частности, определение их семантики.
Структуры данных и их производительность.
Игры и искусственный интеллект.
В основном, конечно, моя работа связана с первой темой. Я много занимался производительностью компиляторов, что, несомненно, связано и со второй темой. Еще в средней школе, я интересовался компьютерами и любил игры. Отсюда появился и третий интерес. Для меня компьютеры всегда были и остаются интересным развлечением. Хотя мне уже не понять всего того, что мой младший сын принимает как должное, имея айпад.
Мой интерес в компьютерных играх можно увидеть в паре статей, которые я написал (с друзьями) про разновидность шахмат, называемую Кригшпиль (Kriegspiel). Они были опубликованы в журнале «Computer Journal». В одной из них была приведена полная программа для проверки правильности ходов в Кригшпиле. На тот момент было несколько обучающих программ для студентов, но в которых, увы, проверялись далеко не все тонкости шахматных правил. Были, конечно, шахматные программы, но до времен открытого программного обеспечения было еще далеко, а авторы шахматных программ не хотели раскрывать своих секретов, так как продавали их. Поэтому, насколько я знаю, моя программа была первой открытой реализацией для проверки шахматных ходов. Может быть, и последней. Здесь, в Силиконовой Долине, об этом проекте есть мое аудио интервью в компьютерном музее.
Обычно, мой подход следующий: сначала я пытаюсь подобрать подходящую теоретическую модель для задачи, а затем разобраться, как применить ее на практике.
За все эти годы вы видели язык программирования, который могли бы назвать лучшим?
На текущий момент мне очень нравится Руби. Я написал много кода на С, но мне не нравится низкий уровень безопасности в нем. Раньше мне нравился XPL, который похож на С (конечно, он был создан независимо), но гораздо более безопасен.
Будучи студентом, я написал одну длинную программу на Сноболе. Я моделировал на нем исследовательскую задачу по оптимизации кода. Получилось записать формальное доказательство теорем прямо в виде процедур на Сноболе. Это было откровением. Так я заинтересовался языками, в которых есть интересные динамические свойства.
Я хотел бы уделить время на изучение Haskell или схожего языка для какого-нибудь хорошего проекта. Уверен, там есть чему поучиться. Надо понимать, что написание простого примера вовсе не означает знание и понимание языка. Используя Руби около года в одном проекте, я каждый день узнаю что-то новое.
Создавая языки типа С, люди хотели быть ближе к компьютерному железу, понимание работы которого было обязательным для любого профессионального программиста в те дни. И, как следствие этого, влияло на учебную программу. Как стоит учить программированию сейчас? Его чисто прикладной стороне?
Я никогда особо не интересовался железом, хотя и писал на ассемблере, в котором даже не было мнемонического обозначения машинных команд в трансляторе.
Мне кажется, что вопрос поставлен неверно. Проблема не в программировании как таковом, а в анализе и процессе разработки. Можно написать все что угодно на любом языке. Вопрос в том, правильно ли выбран язык для конкретной задачи или нет.
Поэтому, я бы учил принципам программирования и затем просил студентов применять их в разных языках. Например, вы знаете, что компилятор Фортрана всегда включает в себя интерпретатор? (для форматов). Более того, в Фортране 77 надо реализовать в интерпретаторе возможность вызова по имени. Или, например, вы захотите добавить механизм исключений в интерпретатор. Видите, базовые вещи могут всплывать в самых неожиданных местах.
(Кстати, это объясняет, почему я добавил этюд про интерпретатор форматов. В свое время мне пришлось написать два разных интерпретатора форматов для Фортрана).
Мы начали интервью с вашей книги. А у вас есть своя «версия» Этюдов? Какие-то книги или люди, которые оказали на вас большое влияние?
Когда я учился в школе, друг моих родителей устроил меня на летом на подработку. В итоге я работал в школьном компьютерном центре. Тогда это означало ленты, перфокарты и подобные вещи. Но следующим летом школьный центр купил IBM 1401. И так как никто вокруг не знал это лучше меня, мне разрешили стать одним из программистов. Через несколько недель я уже работал над программой расчета зарплат для одного крупного государственного проекта.
Этот опыт помог мне получить работу в колледже, где тоже использовали 1401. С тех пор моя жизнь была плотно связана с компьютерами, и я решил получить высшее образование по информатике вместо того, чтобы идти работать после колледжа. Не столько благодаря знаниям, сколько везению, я попал в Корнелльский университет. На факультете мне также повезло с очень отзывчивыми людьми. Возможно, самую большую поддержку в учебе мне оказали Юрис Хартманис и Джон Хопкрофт. Я думаю, что даже сейчас книги Хопкрофта по теории языков являются классикой (в первом или втором издании). Но, как я уже сказал, все на факультете всячески помогали студентам. Если посмотреть на выпускников Корнелла с 60-х по 70-е, то можно увидеть много известных людей.
Если говорить о книгах, моим любимым автором является Кнут. Настоятельно рекомендую его «Искусство программирования». Сначала вы скорее всего мало что поймете. По моей теории эту книгу надо читать снова и снова, по несколько страниц. И с каждым разом будет открываться что-то новое.
Будучи классе десятом, я впервые увидел книгу Ньюмана «Мир математики». Она помогла мне разобраться в истории математики и информатики. Уверен, даже сейчас, для молодежи (не говоря уже о людях постарше), эта книги актуальна.
Все попадают в мир компьютеров по-разному. Возможно, сейчас это значительно проще, так как они везде. Как так получилось, что вы занялись компьютерами во времена мейнфреймов и перфокарт?
Частично я уже ответил в предыдущем вопросе. У моего отца было небольшой механический калькулятор, чем-то похожий на описанный в книге Феликса Клейна. Отцовский появился у нас от компании Ориент (я полагаю, отец получил его во время Второй Мировой), без каких-либо инструкций, и на японском языке. К счастью, цифры были арабские. Я часами пытался в нем разобраться.
В средней школе я уже интересовался компьютерами и математикой. Однажды мне достался игрушечный «компьютер», сделанный из мазонита, скрепок, винтов, цветных лампочек, батареек и проводов. Сейчас это можно было б назвать логическим симулятором, но на нем можно было собирать простые схемы, используя выключатели как входы и лампы накаливания как выходы. Я был в восторге. Я нашел несколько книг про компьютеры того времени, и это было несказанно интересно.
В первый год в старшей школе я поехал в летний лагерь, где мы изучали алгебру, теорию множеств, немного о конечных разностях (для меня все это было загадкой), и даже программировали компьютер. Там я научился играть в бридж, чтобы иметь деньги на девушек (в школе девушки являются сильным мотиватором!).
К моменту окончания школы, благодаря всему вышеописанному и нескольким курсам из колледжа (преимущественно с Томом Читемом), я был уже достаточно увлечен.
Под занавес, есть выражение про три вещи, которые каждый мужчина обязан сделать. Есть ли такие три вещи для программистов?
Не уверен, что могу назвать то, что каждый программист должен сделать. Но я знаю, что каждый программист должен знать или уметь делать.
Иметь познания в формальной математике. Уровня для понимания книг Хопкрофта и Ульмана, дополненного немного теорией графов, будет достаточно. Дискретная математика крайне необходима; математический анализ может понадобится в некоторых областях.
Умение понятно писать на родном языке. Дейкстра говорил, что человек, не умеющий писать на собственном языке, не может писать хорошие программы (надеюсь, Дейкстра это действительно говорил!). Написание программ, по сути, - как написание прозы. Если вы не можете связно излагать на родном языке, сделать это, например, на С, будет еще сложнее.
Стоит помнить, что программа предназначена для общения людей, а не компьютеров. Что вы напишете, то компьютер и сделает. Ему все равно, что именно. Ваша задача убедить других людей, что то, что вы попросили компьютер исполнить, правильно. Помните, компьютеру на правильность наплевать.
(Бонус, четыре по цене трех). Ответ на последний вопрос значит, что программы должны быть в форме понятных комментариев, объясняющих назначение кода, который следует за этими комментариями. Форматирование должно быть таковым, чтобы читателю было легко и просто понять ваш код. А компилятору без разницы. В частности, следуйте соглашениям, принятым в математике и вашем естественном языке, а не вычитанным в каком-то непонятном руководстве по языку программирования. Сначала пишите комментарии, а затем код, и не наоборот. Если вы не знаете точно, что хотите получить и почему, любой код, который вы напишете, будет, по определению, неверным.
Я надеюсь, что если посмотреть на главы «Этюдов», будет видно, что даже в те времена я старался применять эти принципы.
Спасибо, Чарльз, за ответы. Мы, поклонники «Этюдов», все еще надеемся увидеть ваши новые книги. А пока, если вы, дорогие читатели, все еще не знакомы с книгой «Этюды для программистов» Чарльза Уэзерелла, не поленитесь, прочтите. Эта книга того стоит.
■
// Чарльз Уэзерелл, Александр Дёмин
// Август 2012
P.S. Если вы хотите задать какие-то вопросы Чарльзу, можно это сделать в комментариях на странице с оригиналом интервью на английском. Чарльз постарается лично на них ответить.
]]>Например, если нужно число-заглушка для 16-и или 32-x битного указателя, используются слова из следующего набора:
ABBE
ACED
BABE
BADE
BEAD
BEEF
BODE
CADE
CAFE
CEDE
COCA
CODA
CODE
DACE
DADO
DEAD
DEAF
DECO
DEED
DODO
FACE
FADE
FEED
FOOD
OBOE
Например:
DEADFACE
ACEDFEED
Можно пойти дальше и не ограничиваться симметричным разделением. В этом случае слов будет больше:
ABBE
ABE
ABODE
ACCEDE
ACCEDED
ACE
ACED
ADD
ADDED
ADO
ADOBE
BAA
BABE
BAD
BADE
BAOBAB
BE
BEAD
BEADED
BED
BEDDED
BEE
BEEF
BOA
BOB
BOBBED
BODE
BODED
BOO
BOOBOO
BOOED
CAB
CACAO
CAD
CADE
CAFE
CEDE
CEDED
COB
COCA
COCOA
COD
CODA
CODE
CODED
COFFEE
COO
COOED
DAB
DABBED
DACE
DAD
DADO
DEAD
DEAF
DEB
DECADE
DECAF
DECO
DECODE
DECODED
DEE
DEED
DEFACE
DEFACED
DO
DOC
DODO
DOE
DOFFED
EBB
EBBED
EFFACE
EFFACED
FAB
FACADE
FACE
FACED
FAD
FADE
FADED
FED
FEE
FEED
FOB
FOBBED
FOE
FOOD
OAF
OBOE
ODD
ODE
OF
OFF
Например:
BADCOCOA
BADCODED
FADEDDOC
CODEDBOB
Ну а на 64-битной системе можно писать почти предложения.
EBBEDDEADBAOBAB
Кто предложит фразу для следующего, 128-и битного поколения?
]]>Потом был эмулятор под ДОС, потом под Windows, потом на JavaScript’е.
Но мы возвращаемся к истокам, к железу. Итак, четвертая реинкарнация Радио-86РК в новом, на этот раз железом теле. Увы, для “пощупать” надо иметь это железо, поэтому для демонстрации ничего не оставалось, как только записать видео.
К слову сказать, ядро эмуляции I8080 (КР580), которое было у меня аж в самом первом ДОСовском эмуляторе, используется на Maximite почти без изменений.
На всякий случай ссылочка на проект Maximite.
P.S. Кстати, если кто отдаст, продаст или поможет достать родной РК, например, в Москве, буду рад пообщаться.
]]>Если сказать, что я полном отпаде – это ни чего не сказать. Причем, в отпаде я не сколько от материала. Он на высоте, но этим удивить сложно. А в восторге я от формата самого обучения, качества подготовки материалов и онлайновой технологии проведения.
Вкратце.
Курс длится шесть недель. На каждую неделю дается около полутора часов видеолекций, разбитых на эпизоды по 10-20 минут. Видео интерактивное, и в некоторых есть моменты, когда оно останавливается, и предлагаются вопросы для закрепления прослушанного. Для каждой лекций есть две соответствующие PDF’ки со слайдами: одна с надписями вручную, которые по ходу видеолекции появляются постепенно со объяснениями лектора, а вторая с текстом, набранном привычными шрифтами. Видео можно смотреть сколько угодно раз. Можно прямо в браузере, но можно скачать видеофайлы (правда, тут я не уверен, будут ли работать интерактивные вопросы). Есть английские субтитры и транскрипт. А еще у видео, которое смотришь в браузере, есть мега возможность – менять скорость просмотра в пределах от 0.5 до 1.5. Когда начинается много “бла-бла” можно ускорить, на время.
Также на каждую неделю дается список теоретических вопросов и практическое задания. На вопросы надо ответить, а для практики написать программу и засабмитить что требуется. Есть ограничения на количество попыток сдачи. Учитывается лучший данный ответ.
Реально ограничений на время сдачи нет, но ответы и решения, сданные с запозданием (по истечению недели), будут оценивать с 50%-й “скидкой”, поэтому если охота получить сертификат в конце, лучше все сделать вовремя.
В конце – последний тест-экзамен. На него есть ограничение по времени - 3 часа. Поэтому стоит выбрать время так, чтобы все можно было закончить.
Есть форумы, где наряду со студентами появляются и ведущие курса.
Мое личное субъективное мнение, что можно было б сделать курс немного “по-жестче”, но в целом баланс нагрузки для факультативного вечернего время провождения пару раз в неделю замечательный.
Вывод: всем советую. Конечно, не этот конкретно курс (интересы у всех разные), а технологию в целом.
Если сейчас кто-то скажет, что нет денег на обучение в дорогом ВУЗе – посоветуйте найти денег на интернет и недорогой компьютер. Этого достаточно.
Хочу следующим попробовать 6.002x: Circuits and Electronics. Интересно, чем и как они заменят в онлайне практические занятия, без которых электроника невозможна.
]]>
Размеры от 64 мегов до 32 гигов.
И возникла идея сделать из них RAID, например, просто “concatenated disk”, ибо в OSX это делается очень просто. Идеи тут минимум, но было интересно попробовать.
Купил я вот такой девайс на двенадцать сосков.
Satechi 12 Port USB Hub with Power Adapter & 2 Control Switches
Втыкаем…
И запускаем Disk Utility. Все флешки, вроде, видятся.
Создаем диск типа “concatenated disk” с именем “Crazy RAID” и добавляем туда флешки, предварительно их отформатировав в HPFS+ для единообразия.
Подтверждаем…
Ждем минут пять и готово.
В системе видится.
Можно теперь чего-нибудь туда скопировать.
Конечно, конструктивного применения тут реально ноль, ибо все флешки разные и кроме их объединения в один диск сложно что-то еще сделать в плане RAID. Скорость записи будет определяться той флешкой, на которую идет запись в данный момент. При параллельном чтении, возможно, будет ускорение.
Но выглядит приколько!
]]>Далее будет набор выборочных фотографий того, за что у меня лично зацепился взгляд. Естественно, экспонатов там в разы больше.
Настоящая BBS, хоть и на Windows.
Родоначальник(и) эпохи.
NeXT CUBE.
Модем, братья, на 1200! или 300 full duplex.
Матрицу смотрели? Помните, через какой модем они там телепортировались?
Удивительно, если б в британском музее не было бы Raspberry Pi.
Портативный лазеровод.
Эх, купить себе эту легенду что-ли?
Братья! Пристегните ремни. Это целый класс работающих BBC Micro. Книги и мануалы – на выбор. Можно присесть и по программировать.
Что я и сделал.
BBC Micro изнутри.
Везде книги, новые, старые.
RML 380Z
PET Commodore
ELIZA умеет задавать вопросы и отвечать.
История компьютерной памяти, от лампы до SIMM’ов и DIMM’ов.
Флешка из прошлого.
Макетные микрокомпьютеры с кишками наружу (подборочка). У меня к ним слабость.
Хотели б на таком учиться в свое время? Я тоже.
Эргономичные клавиатуры не вчера придумали.
Дорогущий бизнес-компьютер восьмидесятых.
На огромной полке с различными мануалами нежно припаркованы кассеты. Оказывается, их использовали не только для дома и для семьи.
Переходим в полусредний вес. Уже не настольные, а напольные компьютеры.
Cray
PDP-8
PDP-11
Рабочая станция XEROX.
VAX. В годы учебы я его “видел”, точнее представлял его, через призму удаленного терминала.
Графическая станция.
Терминалы, терминалы.
В зале эпохальных персоналок.
Помните этого кроссавца?
В углу? Мы то его в основном “знаем” по схеме Зонова. А это оригинал.
В действии.
Оказывается, что на самом деле машин в формфакторе “клавиатура” было очень много.
Вот эта машина сделала Билла Гейтса знаменитым.
А вот эта – двух Стивов, и позволила создать чудесный Mac Air, с которого я пишу сейчас этот пост, роняя на клавиатуру слезы умиления.
Можно присесть на банкеточку и порубиться в раритетные игрушки. Тут собраны основные эпохальные игровые компьютеры 80-90x.
В отдельной комнате можно порубиться во Flight Simulator.
Мейнфреймы.
UNIX. Начало всех начал.
Реальный монитор радара.
Кстати, какой же компьютерный музей без вайфая.
PDP-11
Аналоговый компьютер. В цифровых компьютерах вы разбиваете задачу на элементарные двоичные операции. В аналоговых вы стоите модель вычислений на основе свойств усилителей и обратных связей, заставляя их складывать электрические сигналы, вычитать, интегрировать, дифференцировать и т.д.
Удобно, однако.
Новый, пока неготовый зал.
Хотя что-то там уже есть.
Машинки для пробивки перфокарт.
А вот пример (внимание) калькулятора!
Архивы журнала Computer Weekly, сделанные вручную, кстати.
Братья! Это нереальный музей. К сожалению, туристам до него сложно добираться, но настоящих фанатов это не должно останавливать. Какая там Trafalgar Square или London Eye? Вот что надо смотреть.
У меня было всего сорок минут на все про все. Чертовски мало. Если удастся, съезжу еще.
]]>
Этот девайс позволяет старые устройства, которые умеют работать, например, только c модемом по RS232 через AT-команды, подключить в мир TCP/IP. Мы такие использовали в бытность мою в Автобанке для подключения POS-терминалом и банкоматов, которые многие под сих пор модемами пользуются, в Ethernet. Меняешь скрипты дозвона, например, ATDT12345678
на что-то типа ATD192.168.1.1
и вперед.
Ума не приложу, что с ним делать. У меня COM-порта то нет нормального, только через USB-шланг. Но в свое время такие устройства денег стоили реальных.
]]>Programming 32-bit Microcontrollers in C: Exploring the PIC32 (Embedded Technology)
Мой личный опыт с микроконтроллерами PICmicro закончился лет десять назад, когда с братом делами автомобильную сигнализацию на основе GSM телефона. В сердце был PIC16, DTFM декодер (уникальная, кстати на тот момент фича, когда можно было рулить сигнализацией, позвонив на ее номер и пикая с трубку циферьки) и датчики.
Потом было еще насколько проектов на PIC12. Меня реально перло, что микруху с несчастными восемью ногами можно программировать, на С!
Потом как-то все засохло. Я переехал, и многое из “добра” не взял с собой. Но вот недавно, после сборки Maximite и покупки Raspberry Pi, вся эта тема вернулась. Но время не стоит на месте, и современные PIC32 уже на ядре MIPS, все из себя 32-х битные, плюс с какой никакой виртуализацией адресов, что позволяет делать несложные операционные системы с разделением ядра и пользовательских процессов (например, RetroBSD, которая, кстати, прекрасно работает на Maximite).
Так вот, возвращаясь к книге. В желании освежить в памяти эту тему, я отрыл ее. К моему удивлению, я, не отрываясь, с удовольствием прочитал ее по диагонали за пару вечеров.
Книга очень грамотно построена и рассчитана на людей с минимальными знаниями о микроконтроллерах. Там даже есть экскурс в язык С для желающих. Начиная “от печки” в виде примитивных примеров с картинками и подробными объяснениями, автор постепенно раскрывает весьма недетский набор тем:
Все примеры законченные и работающие. Особенно будет удобно, если иметь диск от книги.
Одним небольшим неудобством было то, что автор использует среду MPLAB и компилятор С32 (все-таки книга вышла аж в 2008 году), а Microchip последнее время всех активно пересаживает на MPLAB-X, который есть не только для Windows, но и Linux и OSX, и компилятор XC32 на базе GCC. Хотя файлы проектов несложно импортируются в новую среду, в книге явно не хватает объяснений про элементарные makefile’ы. Но это компенсируется подробными картинками окон конфигурации, отладчика и симулятора. В принципе, книгу можно читать даже не имея компьютера под рукой.
Итак, проглядев книгу, вы будете понимать что и как возможно сделать на PIC32. А если решите собрать и запрограммировать реальный проект, у вас будет достаточно начального работающего кода и понимания, как запустить кристалл.
Вывод: настоятельно рекомендуемая книга. Кстати, вышла она не недавно, так что можно найти в хорошем PDF-ном качестве, хотя купив ее, вы негласно скажете автору спасибо.
]]>Сегодня у меня в гостях Джеф Грэхем, создатель популярного самодельного микрокомпьютера Maximite. На данный момент мы наблюдаем бум проектов, основанных на микроконтроллерах. Мы все еще называем их “микроконтроллерами” по привычке, но по сути они ими уже не являются. Например, кристалл PIC32 компании Микрочип содержит в себе больше возможностей, чем в восьмидесятых мы имели в “полноценных” компьютерах типа Эппл 2 или Спектрум 48.
Я занимался электроникой, мини-компьютерами и Бейсиком долгое время. Поэтому, когда я понял, что PIC32 может работать с VGA-дисплеем и клавиатурой, самой собой получилось, что я решил вспомнить молодость и воссоздать один их моих первых компьютеров.
Джеф создал Maximite на основе PIC32. Maximite является полностью законченным бытовым компьютером, оснащенным современными интерфейсами, такими как VGA, PS/2, SD, USB, а управляет всем этим – Бейсик! Вам это ничего не напоминает? По мне, Maximite является превосходным инструментом для изучения микроэлектроники, в котором можно пощупать, где и как железо соприкасается с программным обеспечением.
У нас есть возможность лично задать Джефу несколько вопросов. Джеф живет в пригороде Кенсингтон города Перт в западной части Австралии. Перт весьма далеко расположен от других городов, но благодаря интернету и FedEx’у, расстояния не кажутся такими уж и большими в наши дни, так что это не мешает Джефу в работе над его проектами.
Здравствуйте, Джеф, спасибо за интервью. Почему “Maximite”? Как родилось это название?
Изначально, когда проект начинался, я хотел назвать его “The Mighty Mite”, что значит что-то небольшое (mite) и одновременно что-то “продвинутое” (mighty). Когда проект готовился к публикации в журнале “Silicon Chip”, редакторы выяснили, что предложенное мной имя уже зарегистрировано каким-то производителем продуктов, и предложили изменить его на Maximite. На тот момент у меня не было других идей, и мы приняли это имя. Кроме того, имя стало похоже на название взрывчатки в некотором роде, что тоже было неплохо.
Maximite очень популярен. Вы знаете, хотя бы примерно, сколько их вообще сделано? И как и когда вы вдруг поняли, что проект стал хитом?
Обычно я говорю, что их собрано тысячи, хотя если поточнее, где-то между тремя и четырьмя тысячами. Около половины собраны энтузиастами самостоятельно, а другая половина с помощью готовых плат от компаний, скопировавших разработку.
Я понял, что проект популярен, по статистике посещаемости моего сайта. В первые дни после выхода статьи в журнале количество посетителей увеличилось в сто раз, и после этого постепенно закрепилось где-то на отметке в двадцать раз по сравнению с до Maximite’овскими временами. Забавно, что в первой опубликованной статье не было ссылки на мой сайт, что говорило о том, что люди самостоятельно начинали гуглить после прочтения.
Начиная разрабатывать Maximite, я представлял компьютеры восьмидесятых, с которыми мне довелось поиграться в те времена. Они были очень популярны в свое время до появления более современных моделей, которые стали гораздо сложнее в конструкции и использовании. Ниша простых и понятных компьютеров исчезла.
Для Maximite вы спроектировали железо и написали софт, что весьма необычно в наши дни. Программисты нечасто занимаются железками, а инженеры-железячники – программами. Лично вы кто больше: программист или железячник?
Да, это одна из вещей, с которой я столкнулся с момента выхода Maximite. Люди часто смотрят на проект однобоко: либо они пытаются изменить принципиальную схему без учета прошивки, либо наоборот. Про себя могу сказать, что все-таки я программист, но с хорошим пониманием электроники.
Еще одна вещь, которую люди часто пытаются сделать с Maximite – это изменить его целостность как устройства, его взаимодействие с пользователем, но делаться это должно с учетом и железа и программного обеспечения. Эти три составляющие важны все вместе, что часто упускается из виду и программистами, и специалистами по аппаратной части.
Я лично был поражен, насколько целостным и законченным является проект Maximite. Вы разработали все: прототип, корпус, программное обеспечение. Можно купить даже конструкторы для любителей. В мире любительских проектов люди часто не доводят их до законченной формы из-за потери интереса. Как вы справились с этой проблемой?
Изначально настрой все закончить был из-за желания опубликовать проект в журнале Silicon Chip. Большинство проектов в нем являются законченными профессиональными продуктами, и мне пришлось следовать стандартам. Подобное внимание к деталям весьма необычно в наши дни, и журналы часто не публикуют ни документации, ни модели корпуса, ни тем более красивой лицевой панели.
Мне понравился столь дисциплинированный подход конкретно этого журнала. У меня есть несколько проектов (например, из неопубликованных) в полузаконченном состоянии, на которые у меня не хватает терпения довести все до ума.
Джеф, вы занялись микроэлектроникой после выхода на пенсию. Сколько времени, например, в день вы тратите на это хобби? Опять-таки, чтобы довести проект Maximite до столь вылизанного состояния, видимо, потребовалось ощутимое время.
Да, я действительно потратил на проект много времени после выхода на пенсию, но, что интересно, мне в некотором роде пришлось это сделать. Я почувствовал, что в моей прошлой профессиональной карьере мне не удалось реализовать скрытый во мне потенциал инженера, а вот на пенсии этой реализовалось.
С Maximite все происходило волнообразно. Я работал по двенадцать часов в течении месяца, а потом практически ничего не делал несколько недель. Это здорово, что мне нравится работать над проектами вроде этого, которые требуют много времени. Я бы оценил трудозатраты на Maximite как девять месяцев полноценной работы по восемь часов в день, сорок часов неделю.
MMBasic. Я знаю, что вы пытались начала найти готовую версию Бейсика для Maximite, но после нескольких неуспешных попыток адаптировать некоторые готовые реализации, вы решили написать свою. По какой лицензии распространяется MMBasic? Есть ли планы разработать библиотеку драйверов для аппаратуры Maximite, чтобы можно было проще создавать альтернативные прошивки, например, RetroBSD или Lua?
Да, я начал Maximite в надежде найти открытую версию Бейсика в интернете и использовать ее. Но, увы, так и не нашел подходящей доступной реализации. Везде были какие-то сложности. В итоге, решил написать все сам.
Сначала я выпустил свой вариант Бейсика, названного MMBasic, под лицензией GPL GNU, но был неприятный эпизод, когда некая фирма просто удалила мое имя из авторских прав, выдав программу за свою. В довершение они даже изменили название Бейсика. Я был вынужден создать свою собственную лицензию, по которой код являет открытым, но не может распространяться без моего согласия.
Это сработало, и сейчас несколько университетов и коммерческих организаций используют MMBasic по договоренности со мной на тему вариантов использования кода. Это намного лучше, чем разрешать кому угодно анонимно скачивать исходные тексты, а потом видеть их в сети под чужим именем и авторскими правами.
На данный момент у меня нет планов создавать драйвера для аппаратуры Maximite. Главной причиной тому является ограниченность объема памяти PIC32 на сегодняшний день. Одновременно для всех устройств (VGA, клавиатура, USB и т.д.) просто нет места сделать полноценные загружаемые драйвера. Можно было бы использовать более мощный микроконтроллер, но я хотел бы пока остаться с PIC32. Для компьютеров типа Raspberry Pi можно сделать загружаемые драйвера, но это требует гораздо более развитой операционной системы, а мне хотелось бы сохранить Maximite простым.
Немного о вашем профессиональном прошлом. Например, когда и как вы первый раз взяли в руки паяльник? Я знаю многих людей, которые были б не против заняться электроникой, но для которых факт пайки выглядит очень трудной или даже невозможной задачей. Чтобы вы посоветовали таким людям в качестве отправной точки?
Не могу точно вспомнить, когда я впервые взял в руки паяльник, возможно мне было около двенадцати. Может я не самый удачный консультант в этом вопросе, но как мне кажется, все что нужно для пайки – это недорогой паяльник и немного припоя.
Одна из хороших сторон увлечения электроникой, что это весьма дешевое хобби. Я бы посоветовал начать с чего-нибудь простого, например, схемы бегущего огонька или добавления часов в Ардуино, попутно купив, непосредственно, паяльник. Конечно, первый блин может выйти комом, но это неважно, так как к проекту третьему вы будете чувствовать себя почти профессионалом.
Можете посоветовать какие-нибудь книги или веб-сайты для начинающих, по которым можно сделать первые шаги в электронике? И какой, например, уровень подготовки нужен для сборки или создания проекта типа Maximite?
Я бы посоветовал начать с журналов, например Silicon Chip (Австралия), Elektor (Европа) и Nuts and Volts (США). На все эти журналы можно подписаться и из-за границы. Если вы полный новичок, то скорее всего Nuts and Volts подойдет лучше всего. Подписавшись на журнал, вы будете постоянно иметь поток свежих идей для проектов, и, возможно, придумаете что-нибудь свое. Так что подписаться однозначно стоит.
Уровень знаний для изготовления Maximite нужен не такой уж и большой, так как все уже готово. Конечно, для создания подобного проекта с нуля безусловно требуется некоторый опыт. У меня он был. Изначально я учился на инженера-электронщика, а затем тридцать лет работал в области системного программирования – так что у меня были все необходимые знания.
Сейчас существует множество веб-сайтов, на которых частные разработчики железа могу продавать свои идеи и готовые проекты, например, SparkFun.com. Интернет позволяет им за свое хобби получать деньги. Как человек, чьи разработки активно продаются в интернете, как вы считаете, возможно ли на подобных сайтах зарабатывать на жизнь?
Будет очень трудно заработать достаточное количество денег таким образом. Некоторым разработчикам, например, создателям Arduino, это удалось, но большинству все-таки приходится иметь постоянную работу еще где-то. Отчасти проблема в том, что люди как я, создающие сложные устройства и затем бесплатно их распространяющие, не могут реально конкурировать. И покупатели привыкли получать результаты их труда по очень низким ценам.
Расскажите немного лично о себе. Есть ли какие-то люди, например, инженеры или программисты, у которых вам довелось или хотелось бы поучиться?
Если говорить о программистах, то для меня это Брайн Керниган, который вместе с Деннисом Ритчи создал язык С и написал книгу “The C Programming Language” в 1978 году. До сих пор эта книга является прекрасным введением в С и стоит того, чтобы ее прочитать. Когда я пишу статьи для журналов, я стараюсь писать в таком же простом и понятном стиле.
Джим Роув, возможно, является инженером, оказавшим на меня значительное влияние. Он писал статьи для журналов по электронике в Австралии, когда я был подростком. Спустя пятьдесят лет, он все еще продолжает этим заниматься, и его проекты и статьи всегда грамотно спроектированы, и при этом просты и понятны. Он еще один человек, кому я стараюсь подражать.
Кстати, что привело вас в мир компьютеров и электроники? Каким был ваш первый компьютер, и на каком языки вы начали программировать?
Давайте попробуем сориентироваться во времени (мне сейчас 64). Я помню, как появился Altair, и немногим позже компания, где я работал, стала дистрибутором Intel в Австралии. Я учился на инженера-электронщика и свой первый компьютер спаял сам. Он был на процессоре Intel 8080 и имел 512 байт памяти.
Тогда Бейсик был, пожалуй, единственным языком программирования из-за ограниченных аппаратных возможностей и небольшого объема памяти. Гораздо позже, когда появились жесткие диски, мы стали использовать более сложные языки, и в свое время я программировал на большинстве из них (Fortran, COBOL, PL/1, ассемблер, Pascal и т.д.)
Как я уже говорил, я занимался электроникой, мини-компьютерами и Бейсиком долгое время. Поэтому, когда я понял, что PIC32 может работать с VGA-дисплеем и клавиатурой, самой собой получилось, что я решил вспомнить молодость и воссоздать один их моих первых компьютеров.
Под конец, не могли бы вы рассказать немного о своих планах. Будет ли новая версия Maximite, например, с Ethernet или WiFi?
В сентябрьском выпуске журнала Silicon Chip будет опубликован проект нового Maximite с поддержкой цветного дисплея. Он будет поддерживать восемь цветов на VGA-дисплее, синтезированный стерео звук и иметь разъем, совместимый в Arduino, плюс все остальные возможности оригинального Maximite (USB, Бейсик и т.д.). Данный проект занял у меня прилично времени и сил, так что я после публикации хочу немного отдохнуть, прежде чем браться за что-то новое.
Вот пара эксклюзивных фотографий нового цветного Maximite.
Спасибо, Джеф, за интервью и за проект Maximite. Очень ждем ваших новых разработок.
Лично я очень рад, что в последнее время люди стали понимать, что обучение компьютерам, это не только Word и Excel, а также знания о том, как все это работает изнутри. И такие проекты, как Raspberry Pi или Maximite очень помогают новичкам понять, как работает “железо” компьютеров.
■
// Джеф Грэхем, Александр Дëмин
// Август 2012
]]>Palm-Size Portable Mini Travel 2 in 1 Wireless N 802.11n/g WLAN Network Router / Client Adapter
Называется WA-6220.
На официальном сайте пишут:
WA-6220-V1 WLAN 11n Router is designed for people on the go, user can carry it travelling around and work at anywhere. It is compliant with 802.11n specifications, up to 300Mbps data rate, provides multi-functional capabilities, particularly the high performance throughput and high-quality security. Incorporating fast Ethernet ports and is compatible to other wireless (802.11a/b/g/ n) networking device, It enables your whole network sharing a high-speed cable or DSL. Its high performance is ideal for media-centric applications like streaming video, gaming and Voice over IP technology.
Размером со спичечный коробок.
Питается от разъема USB. Я подключал прямо у ноуту.
С лица.
Переключатель режимов и кнопка сброса.
Краткая инструкция.
Увы, лично я не встречал ни одной гостиницы, где был бы WiFi в номере без геморроя. Провод кинуть могут все, а вот нормальное беспроводное покрытие – никто. А с этим устройством (которое, кстати, может питаться прямо от USB ноутбука) – ты сам себе переносная WiFi-точка.
P.S. Кстати, один вопрос меня давно волнует. Железо, стоящее, например, в ноутах и полноценных WiFi-точках доступа, принципиально отличается? Ведь если канал двунаправленный – вроде не должно же. Если так – как сделать из ноута полноценную точку доступа? Я пробовал на Windows 7 использовать программы типа Connectify. Увы, реально это не работало. А есть же еще Linux и OSX. Как там быть?
]]>К делу. Сегодня речь пойдет о пайке. Знаю, что многих новичков, желающих поиграться с микроконтроллерами, это отпугивает. Но, во-первых, можно воспользоваться макетными платами, где просто втыкаешь детали в панель, без даже намека на пайку, как в конструкторе.
Так можно собрать весьма кучерявое устройство.
Но иногда хочется таки сделать законченное устройство. Опять-таки, не обязательно “травить” плату. Если деталей немного, то можно использовать монтажную плату без дорожек (я использовал такую для загрузчика GMC-4).
Но вот паять таки придется. Вопрос как? Особенно, если вы этого никогда раньше не делали. Я, возможно, открою Америку, но буквально несколько дней назад я сам для себя открыл волшебный мир пайки без особого геморроя.
До сего времени мое понимание сути процесса ручной пайки было следующим. Берется паяльник (желательно с жалом не в форме шила, а с небольшим уплощением, типа лопаточки), припой и канифоль. Для запайки пятачка, ты берешь капельку припоя на паяльник, макаешь паяльник в канифоль, происходит “пшшшшш”, и пока он идет, ты быстро-быстро касаешься паяльником места пайки (деталь, конечно, должна быть уже вставлена), и после нескольких мгновений разогрева припой должен каким-то волшебным образом переходить на место пайки.
Увы, у меня такой метод работал очень плохо, практически не работал. Детали нагревались, но припой никуда с паяльника не переходил. Очевидно, что проблема была в катализаторе, то есть канифоли. Того “пшшшшш”, что я делал, опуская конец паяльник в канифоль, явно не хватало, чтобы “запустить” процесс пайки. Пока ты тащишь паяльник к месту пайки, вся почти канифоль успевает сгореть. Именно поэтому, кстати, мне была совершенно непонятна природа припоя, внутри которого уже содержится флюс (какой-то вид катализатора, типа канифоли). Все равно, в момент набирания припоя на паяльник весь флюс успевает сгореть.
Экспериментальным путем я нашел несколько путей улучшить процесс:
Итак, мы почти уже у цели. Я так подробно все пишу, так как, честно, для меня это было прорыв. Как я случайно открыл, все, что нужно для пайки несложных компонент – это паяльник, самый обычный с жалом в виде шила:
и припой c флюсом внутри:
ВСЕ!
Все дело в процессе. Делать надо так:
Ключевой момент тут, как вы уже поняли, это подача припоя и флюса прямо на место пайки. А “встроенный” в припой флюс дает его необходимое минимальное количество, сводя засирание платы к минимуму.
Ясное дело, что время ожидания на каждой фазе требует хотя бы минимальной практики, но не более того. Уверен, что любой новичок по такой методике сам запаяет Maximite за час.
Напомню основные признаки хорошей пайки:
Стоит заметить, что все выше сказанное относится к пайке элементов, которые вставляются в отверстия на плате. Для пайки планарных деталей процесс немного более сложен, но реален. Планарные элементы занимают меньше места, но требуют более точного расположения “пятачков” для них.
Планарные элементы (конечно, не самые маленькие) даже проще для пайки в некотором роде, хотя для самодельных устройств уже придется травить плату, так как на макетной плате особого удобства от использования планарных элементов не будет.
Итак, небольшой, почти теоретический бонус про пайку планарных элементов. Это могут быть микросхемы, транзисторы, резисторы, емкости и т.д. Повторюсь, в домашних условиях есть объективные ограничения на размер элементов, которых можно запаять обычным паяльником. Ниже я приведу список того, что лично я паял обычным паяльником-шилом на 220В.
Для пайки планарного элемента уже не получится использовать припой на ходу, так как его может “сойти” слишком много, “залив” сразу несколько ножек. Поэтому надо предварительно в некотором роде залудить пятачки, куда планируется поставить компонент. Тут, увы, уже не обойтись без жидкого флюса (по крайне мене у меня не получилось).
Фаза 1
Капаете немного жидкого флюса на пятачек (или пятачки), берете на паяльник совсем немного припоя (можно без флюса). Для планарных элементов припоя вообще надо очень мало. Затем легонько касаетесь концом паяльника каждого пятачка. На него должно сойти немного припоя. Больше чем надо, каждый пятачек “не возьмет”.
Фаза 2
Берете элемент пинцетом. Во-первых, так удобнее, во-вторых пинцет будет отводить тепло, что очень важно для планарных элементов. Пристраиваете элемент на место пайки, держа его пинцетом. Если это микросхема, то надо держать за ту ножку, которую паяете. Для микросхем теплоотвод особенно важен, поэтому можно использовать два пинцета. Одним держишь деталь, а второй прикрепляешь к паяемой ножке (есть такие пинцеты с зажимом, которые не надо держать руками). Второй рукой снова наносишь каплю жидкого флюса на место пайки (возможно немного попадет на микросхему), этой же рукой берешь паяльник и на секунду касаешься места пайки. Так как припой и флюс там уже есть, то паяемая ножка “погрузится” в припой, нанесенный на стадии лужения. Далее процедура повторяется для всех ног. Если надо, можно подкапывать жидкого флюса.
Когда будете покупать жидкий флюс, купите и жидкость для мытья плат. Увы, при жидком флюсе лучше плату помыть после пайки.
Сразу скажу, я ни разу не профессионал, и даже не продвинутый любитель в пайке. Все это я проделывал обычным паяльником. Профи имеют свои методы и оборудование.
Конечно, пайка планарного элемента требует куда большей сноровки. Но все равно вполне реально в домашних условиях. А если не паять микросхемы, а только простейшие элементы, то все еще упрощается. Микросхемы можно покупать уже впаянные в колодки или в виде готовых сборок.
Вот картинки того, что я лично успешно паял после небольшой тренировки.
Это самый простой вид корпусов. Такие можно ставить в колодки, которые по сложности пайки такие же. Эти элементарно паяются по первой инструкции.
Следующие два уже сложнее. Тут уже надо паять по второй инструкции с аккуратным теплоотводом и жидким флюсом.
Элементарные планарные компоненты, типа резисторов ниже, весьма просто паяются:
Но есть, конечно, предел. Вот это добро уже за пределами моих способностей.
Под занавес, пару дешевых, но очень полезных вещей, которые стоит купить в дополнение к паяльнику, припою, пинцету и кусачкам:
Успехов в пайке! Запах канифоли – это круто!
]]>If you ask me, …
Speaking of …,
As I was saying, …
But further - it is more. – Дальше - больше
I very much appreciate discussion. – Я очень ценю дискуссию.
Besides (in addition, furthermore, moreover) – К тому же, помимо
Everything, except the former – Все, кроме последнего
Exactly for this purpose – Именно для этого
Hence – Следовательно
Hardly – Вряд ли
By the way… – Между прочим.
A drop in the bucket – Капля в море
And so on and so forth – И т.д. и т.п.
As drunk as a lord – Пьян в стельку
As I said before… – Как я говорил…
As innocent as a babe unborn – Совсем как ребенок
As sure as eggs is eggs – Как дважды два
As to… (As for…) – Что касается…
Believe it or not, but – Верите или нет, но
Did I get you right? – Я правильно понял?
Don’t mention it – Не благодарите
Don’t take it to heart – Не принимай близко к сердцу
Forgive me, please, I meant well. – Извините, я хотел как лучше
He is not a man to be trifled with – С ним лучше не шутить
I am afraid you are wrong – Боюсь, что Вы не правы
I didn’t catch the last word – Я не понял последнее слово
I mean it – Именно это я имею в виду
I was not attending – Я прослушал
If I am not mistaken – Если я не ошибаюсь
If I remember rightly – Если я правильно помню
In other words… – Другими словами
In short… – Вкратце
It does you credit – Это делает вам честь
It doesn’t matter – Это не важно
It is a good idea – Это хорошая мысль
It is new to me – Это новость для меня
Let us hope for the best – Давайте надеяться на лучшее
May I ask you a question? – Могу я спросить?
Mind your own business – Занимайся своим делом
Most likely – Наиболее вероятно
Neither here nor there – Ни то, ни се
Next time lucky – Повезет в следующий раз
Nothing much – Ничего особенного
Oh, that. That explains it – Это все объясняет
On one hand … – С одной стороны
On the other hand … – С другой стороны
Say it again, please – Повторите еще раз, пожалуйста
That’s where the trouble lies – Вот в чем дело!
Things happen – Всякое бывает
What do you mean by saying it? – Что Вы имеете ввиду?
What is the matter? – В чем дело?
Where were we? – На чем мы остановились?
You were saying? – Вы что-то сказали?
Лично я регулярно отвисаю на engvid.com. Там есть просто шедевральные уроки, особенно в разделе slang.
]]>
Это USB Bit Whacker. Сборка на PIC18, которая со своей стандартной прошивкой позволяет управлять пачкой входов и выходов через простейший “птичий” язык, команды которого подаются через виртуальный последовательный порт.
Схема.
Сначала макет.
Травить плату было лень, поэтому слепил на макетной. Выглядит неказисто, но работает.
В работе.
Все таки GMC-4 – вещь!
P.S. Выложил исходник на Github – gmc4-loader. Если написать на Питоне или Руби, будет еще проще. А программирование последовательных портов в UNIX – это круто.
]]>Я недавно приобрел его книгу о многопоточности в С++. Если честно, я пока не встречал лучшего материала по модели памяти в C++ 2011. Сейчас у нас возможность задать Энтони несколько вопросов, в частности про С++.
Предупреждение: Данная статья является переводом с английского. Я не профессиональный переводчик, поэтому в тексте могут встречаться мелкие неточности. Желающие всегда могут прочитать оригинал на английском.
До тех пор, пока многопоточный код не будет очевидно быстрее, создание однопоточного кода избавит вас от значительной головном боли.
Здравствуйте, Энтони, спасибо, что согласились дать интервью. Разрешите мне начать издалека. Программирование, компьютеры, С++… Почему вы решили этим заниматься? Что привело вас на путь битов и байтов.
Я всегда интересовался компьютерами. И к тому же у меня получалось. Дома у нас был Sinclair ZX81 и BBC Micro в школе, когда мне было 7-8 лет, и вот там все и началось. Разбирался с играми, пытаясь понять, как они работают. Когда пришло время устраиваться на работу после учебы, я уже весьма четко представлял, что буду заниматься программированием. Это приколько, интересно, заставляет думать, и тебе еще за это платят.
Я подозреваю, что С++ не был первым языком программирования, который вы выучили. Приходилось программировать на необычных языках поначалу?
С++ там не было, когда я начал программировать, поэтому это мой не первый язык. Я начал с Бейсика. Все домашние компьютеры снабжались разновидностями Бейсика, и тогда я еще не знал, что существует что-то иное. Некоторые программы были в машинных кодах, и я научился переводить на них язык ассемблера Z80 и вводить из в виде операторов DATA в программе-загрузчике на Бейсике.
Не думаю, что я программировал на чем-то по-настоящему необычном, хотя приходилось поработать на многих языках и на разнообразных системах. Программировать на PICе, имея только 100 байт на код, было непросто, и я подозреваю, не много людей программировали Psion Organizer II, хотя язык программирования там был во многом близок к Бейсику.
Можете вспомнить какие-то значительные или может даже выдающиеся книги, которые повлияли на вас в карьере?
Хм… Это могло быть описание программирования для Z80, когда мне было 10-11 лет. Не могу вспомнить ее названия, но я буквально вызубрил эту книгу. Я помнил каждую инструкцию, включая шестнадцатеричные коды и количество тактов процессора. Я нашел схожее описание для 8086 на диске от PC, распечатал его и также вызубрил.
Сейчас у меня на полке стоят, например, Design Patterns, Refactoring, и The Art of Computer Programming, хотя я все еще люблю быть поближе к железу, например, с Intel’s Software Optimization Cookbook.
Теперь С++ и многопоточность. Было бы неразумно упустить возможность спросить про это у эксперта. Для начала: есть что-то в С++ 2011, что вам не нравиться в плане поддержки многопоточности?
Хороший вопрос! Сходу не могу назвать ничего, про что бы сказал “Мне не нравится, как это сделано”. Немного огорчает, что функция “is_ready()” была удалена из future и shared_future. Хотя ее можно реализовать через wait_for(seconds(0)).
Вы мейнтейнер библиотеки boost::thread. Одновременно с этим вы разрабатываете собственную библиотеку, just::thread. Что в ней такого особенного?
Just::Thread - это строгая реализация библиотеки потоков C++ 2011, значительно оптимизированная для каждой платформы, тогда как в Boost.Thread отдается предпочтение переносимости, и местами ее интерфейс и семантика отличаются от Стандарта. Например, до сих пор отсутствует std::async, который реализован в Just::Thread.
Just::Thread также имеет специальный режим сборки для выявления deadlock’ов.
Actor’ы и идея разделение ресурсов через сообщения. В С++ 2011 их явно их не хватает. Можете порекомендовать библиотеку, где эта функциональность есть?
Just::thread Pro, которая сейчас на стадии разработки, реализует Actor’ы. В моей книге есть пример использования очередей сообщений для создания кода в стиле Actor’ов.
Я помню ваше высказывание, что целесообразность использования многопоточности может быть нетривиальна, так как смысл в ней начинает появляться только с определенного объема данных. Можете посоветовать, как решить - стоит ли озадачиваться многопоточностью в каком-то конкретном случае, или нет?
Да, надо всегда принимать во внимание накладные расходы при многопоточности. Если какая-то задача и так работает достаточно быстро, то лучше ее так и оставить однопоточной. До тех пор, пока многопоточный код не будет очевидно быстрее, создание однопоточного кода избавит вас от значительной головном боли.
Как и всегда – ключевая идея оптимизации производительности (а тут мы рассматриваем многопоточность именно как оптимизацию) - это профилирование приложения. Где больше всего тратиться времени? Как можно распараллелить? Джейсон МакГинесс (Jason McGuiness) продемонстрировал на ACCU 2012, что если распараллелить не ту часть приложения, то много сил уйдет впустую без ощутимого результата.
Давайте теперь поговорим о TDD. Используете ли вы Test Driven Development? Применимо ли это для разработки многопоточных библиотек, например, для C++
Я люблю TDD и стараюсь использовать этот подход все время. Это стимулирует работу небольшими шагами, а набор уже разработанных тестов гарантирует, что существующий функционал “не сломан” новым кодом.
Для многопоточных библиотек применение TDD может быть не совсем прозрачно, но все же возможно. Главное, это выстроить код так, чтобы тестировать то, что нужно. Обычно, требуется что-то по типу барьера (я часто использую std::promise и std::share_future), когда подготавливаете потоки в нужно состояние и говорите “поехали!”. Лучший способ тестирования многопоточного кода — это убрать многопоточность. Вместо этого обеспечить прозрачные механизмы обмена между потоками, и тестировать их по отдельности.
Считаете ли вы, что код обязан быть безупречным, без компромисов к неаккуратности или отсутствия красоты? Как вы решаете для себя, когда код готов к релизу или нет?
Всегда стоит стремиться, чтобы код бы идеален. Если этого не делать, то это обычно заканчивается корявым немодифицируемым кодом.
Однако, не всегда получается писать идеальный код. Порой не ясно, как можно сделать лучше, или надо очень много времени на изменения. Всегда лучше иметь код, которые работает правильно, нежели который выглядит красиво. Если есть достаточно тестов, то можно поработать над красотой кода позже, без опаски что-нибудь сломать.
Я лично сужу о готовности кода по невозможности найти потенциальных путей его сломать. Если таковы пути находятся, я пишу тест и исправляю проблему.
Можете назвать три самый больших “Никогда этого не делай!” для программистов? на С++?
Хех, “Никогда” – это сильное слово.
Вот некоторые вещи, которые не стоит сделать часто (в произвольном порядке):
Использование глобальных переменных. Передавайте все как параметры или используйте члены класса вместо глобальных переменных, так как они делают код сложным для понимания.
Использование синглтонов. Опять, это глобальные переменные, которых следует избегать.
Написание многопоточного кода без тщательного продумывания методов доступа к данным их разных потоков. На это стоит потратить время, так как оно окупится в далекой перспективе.
Конкретно для С++, не стоит часто:
Использовать malloc и free. Это С++, я не С.
Писать код, требующий использования “delete”. Если вам приходится использовать “new”, то стоит использовать умные указатели, например, std::shared_ptr или std::unique_ptr для управления памятью. Также часто лучше просто взять std::vector для хранения чего угодно.
Перегружать операторы нетрадиционным образом. Иногда это может быть действительно удобно (например, << для вывода в поток), но если a+b значит что-то отличное от сложения, то это прямой путь все запутать.
Вы написали книгу. Можно об этом немного поподробнее. Сколько времени заняло создание “C++ Concurrency in Action”, и почему вы вообще решили взяться за ее написание? Что было самым трудным?
С книгой просто все удачно сложилось. Я плотно занимался сообразными главами стандарта С++ и оказался в нужном месте в нужное время.
В итоге, все заняло четыре года. Так как стандарт еще на был принят, когда я начал писать книгу, мне приходилось перерабатывать главы по мере изменения стандарта. Хотя и без этого это была весьма объемная работа. Самым непростым было переписывать некоторые глава после получение отзывов.
Можете порекомендовать еще книги про многопоточность и многозадачность?
“Patterns for Parallel Programming” от Mattson, Sanders и Masingill является хорошим обзором по созданию параллельных программ.
“The Art of Multiprocessor Programming” от Herlihy и Shavit тоже неплохо, но более на низком уровне. Тут описываются такие понятия, как видимость, атомарность и согласованность, и реализация низкоуровневых структур таких как очереди, спин-блокировки и мониторы.
А теперь вопрос, который я спрашиваю всех: спортивное программирование и соревнования по программированию. Важно но ли для любого разработчика регулярно тренироваться в решении алгоритмических задач? И приходилось ли вам разрабатывать или реализовывать замысловатые алгоритмы?
Я люблю всякие задачки. Если разобраться, программирование по себе является большой одной задачей, что и делает его интересным.
Я думаю, головоломки требуют схожего мыслительного процесса как и программирование, поэтому регулярное их решение положительно повлиять на “умение” программировать. Любая практика в чем-либо всегда полезна, и задачи и соревнования по программированию позволяют вырваться из привычных шаблонов и попробовать что-то еще. Мне нравится “Intel Threading Challenge” последние пару лет. Даже не учавствуя напрямую, полезно поработать с задачами – это забавно и реально заставляет задуматься.
Я получил большое удовольствие, работая над алгоритмом, помогающим вертолетным инженерам проводить обслуживание. Они снимали показания с вертолета, используя специальное оборудование в процессе полета при разных условиях. Затем программа анализировала пути улучшения полета. Так как реальные полеты на вертолете дороги, была задача настроить вертолет за минимальное количество пробных полетов.
Вы предпочитаете IDE или vi/make? Возможно ли все еще, используя vi, создавать программное обеспечение современного уровня сложности, или среды разработки являются неизбежным шагом для создания программ промышленного уровня?
Я использую emacs и make. Я пока еще не нашел среды разработки лучше. Уверен, что в Eclipse/СDT очень много потенциала, особенно благодаря усилиям команды Питера Соммерлада, но для меня этого пока недостаточно.
Существует ли идеальный язык программирования? Может ли он вообще существовать? Может это C++?
Всегда есть моменты, которые можно было бы улучшить в любом языке. Не думаю, что идеальный язык существует. Если удастся создать достаточно развитый искусственный интеллект, то мы в итоге придем к использованию естественных языков, нежели языков программирования. Но, я думаю, до этого еще далеко.
Давайте поговорим о найме на работу. Если вам надо нанять хорошего С++ программиста, какие вопросы вы бы спрашивали?
В некотором роде это зависит от того, для чего именно я нанимаю. Если есть время обучить человека, то конкретные знания по С++ не проблема, и я бы сфокусировался на том, что называется “общие способности”.
Если же требуется кто-то, знающий С++, можно и конкретно по С++ спросить.
Под занавес, можете назвать три вещи, которые, по вашему мнению, каждый разработчик просто обязан написать?
Не совсем уверен про три вещи для каждого разработчика, но однозначно стоит попробовать реализовать базовые структуры данных, например, список или хеш. Пусть это будет номер один.
Второе, я думаю, что парсер языка может дать хорошее понимание того, как надо использовать структуры данных, даже если это парсер для разбора простого конфигурационного файла, а не языка типа С++. Например, парсер арифметических выражений может быть неплохим упражнением.
И наконец, я думаю, стоит написать клиент-серверное приложение. Например, вэб-приложение на JavaScript, работающее в браузере, и что-то на северной стороне. Хотя любая задача, где делаются вызовы со стороны клиента к серверу также подойдет. Суть тут в том, что удаленные вызовы всегда заметно дороже локальных, поэтому придется подумать о накладных расходах при создании такого интерфейса. Если речь идет о пользовательском интерфейсе, то это может быть упражнением для минимизации задержек на сервере, что бы интерфейс не “замирал”.
Спасибо, Энтони, за интервью. С нетерпением ждем ваших новых выступлений и книг, и появления на конференциях
■
// Энтони Уильямс, Александр Дёмин
// Июль 2012
]]>Сейчас тут музей криптографии и шифрования. Недавно Колоссус был восстановлен, и можно посмотреть его в действии. У меня была цель посмотреть Колоссус и Энигму.
В этот день была как назло нормальная английская погода – лил дождь. Мой любимый билайновский зонт пришелся как всегда кстати.
Тут старались сохранить дух времен войны – если б не наклейка на стекле, сошло бы за фотографию того времени.
Иллюстрации в прошлом всегда выполнялись с любовью и внимаем к деталям.
Какой-то револьвер. Глаз зацепился, так как он был так близко.
Интересно, что лично я “Шмайсер” видел так близко первый раз в жизни.
Вам это ничего не напоминает? Обжимку для витой пары? Это устройство времен Второй Мировой.
Видимо, это диск-ключ Энигмы, вернее какой-то энигма-подобной машины.
Собственно, Энигма, вернее их тут очень много. Я то представлял себе, что Энигма - это конкретный аппарат, который я знаю по американскому ремиксу фильма “Das Boot”. Оказывается, их выпускали все кому не лень.
Шифровальный аппарат от Сименса.
Или вот уже портативная версия.
Тут все экспонаты реальные, им по 70 лет и более. Их просто вежливо просят руками не трогать.
А вот машина Bombe, конкретно используемая для взлома кода Энигмы.
Вид сзади.
Ее фрагмент в действии.
В радиорубке.
И вот я уже на полпути к заветной цели – Колоссусу.
И так, вот Он – супер-секретный компьютер британских спецслужб времен войны. Усилиями энтузиастов девайс был недавно восстановлен и запущен. Представляет собой две панели, набитые в основном лампами. Это передняя панель, слева.
Передняя панель, справа.
Сзади справа. Так как все это трещит и шевелится (перфоленты, ролики всякие), то стоит реальный гул. Напольные вентиляторы уже нашего времени охлаждают лампы.
Сзади слева.
С боку между панелями. На осциллографе даже показывается какой-то сигнал.
Как работает Колоссус.
Если честно, то я как-то не впечатлился. Ну гудит, ну что-то там ездит туда-сюда. Наверное, если б можно было б программу закодить прямо на месте, может бы и вставило бы.
В том же здании, где стоит Колоссус, находится The National Museum of Computing. Увы, сегодня он был закрыт. Даже взятка в размере 50$ лично в руки дежурному не состоялась. А так хотелось заглянуть в зал мейнфреймов и персоналок.
Грустная фотка через решетку.
В любом музее есть всегда магазин сувенирки. Тут, на удивление, процентов на 80 – книги про историю криптографии. А вот то, что должно быть в любом уважающем себя книжном магазине – диван.
]]>
GMC-4 – это четырехбитный микрокомпьютер. Характеристики компьютера:
Вся архитектура описывается одним небольшим документом – Programming the Gakken GMC-4 Microcomputer.
Сегодня приехала моя посылка. GMC-4 является приложением к одному из выпусков журнала “Otona no Kagaku” издательства Gakken, 24-й номер за 2009 год. Коробка с конструктором приклеена к журналу. Все на японском.
Забавно, на обратной стороне коробки изображены машинные инструкции GMC-4. Все умещается в одну таблицу.
Плата, пищалка, подставка, фрагменты клавиатуры и винты.
Инструкция на японском – это жестко.
Ставим батарейки и закрепляем пищалку.
Плата.
Наклеиваем клавиатуру.
Включаем… It’s alive! Alive!!!
Итак: клавиатура, семисегментник, под ним процессор, затем пипка “hard reset”. Сверху семь светодиодов, управляемых индивидуально. Они, например, используются для отображения текущего адреса. Мне это все напомнило ЮТ-88. В его первой модификации был только семисегментный индикатор и похожая клавиатура.
Попробуем чего-нибудь написать. Например, простая программа, которая ждет нажатия на клавиатуре и затем отображает код нажатой кнопки на семисегментном индикаторе.
00: 0 KA 0 ; Считываем код кнопки (0-F) в A. Если нажата, то Flag=0, иначе Flag=1.
01: F00 JUMP 00 ; Если Flag=1 (не нажата), то переходим на 00.
04: 1 AO ; Выводим A на семисегментник и устанавливаем Flag=1
05: F00 JUMP 00 ; Если Flag=1 (тут всегда 1), то переходим на 00.
Для ввода жмем: RESET 0 INCR F INCR 0 INCR 0 INCR 1 INCR F INCR 0 INCR 0 INCR RESET
Для запуска: RESET 1 RUN
Интересно, эта железка позволяет пошагово трассировать программу. Если запустить программу через RESET 6 RUN
, то она будет останавливаться после каждой инструкции, и текущий адрес будет на верхних светодиодах в двоичном коде. Для выполнения следующей инструкции надо нажать INCR
и т.д. Можно прервать программу, нажав RESET
, и, например, посмотреть содержимое регистров. Они являются ячейками памяти. Какая никакая отладка.
Теперь программа по сложнее: бегущий огонек по верхним семи светодиодам с отображением текущего номера на семисегментнике.
00: 80 TIA 0 ; A=0
02: 1 AO ; Выводим A на семисегментник.
03: 3 CY ; Сохраняем A в Y.
04: E1 CAL SETR ; Зажигаем светодиод, номер которого в Y.
06: 84 TIA 4 ; A=4
08: EC CAL TMR ; Задержка (A+1)*0.1 секунд (0.5 сек).
0A: E2 CAL RSTR ; Гасим светодиод, номер которого в Y.
0C: 3 CY ; Восстанавливаем A из Y.
0D: 91 AIA 1 ; A = A + 1
0F: C7 CIA 7 ; Если A=7, то Flag=0. Иначе Flag=1.
11: F02 JUMP 02 ; Переход на 02, если Flag=1. Также устанавливаем Flag=1.
13: F00 JUMP 00 ; Безусловный переход на 00 (Flag=1 после предыдущей команды).
Коды: 8 0 1 3 E 1 8 4 E C E 2 3 9 1 C 7 F 0 2 F 0 0
Увы, почти все ресурсы на японском, но Google Translate творит чудеса.
Классная игрушка. Я начал программировать на Радио-86РК в машинных кодах, набивая их в Мониторе. Тут почти такие же ощущения. Выпуск журнала, приложением которого является GMC-4, судя по картинкам (я по-японски не читаю, увы), содержит иллюстрированную историю микропроцессоров, начиная с Intel 4004. Далее идут десятки различных примеров программ и проектов, в которых используется GMC-4. Красота!
]]>Но иногда, уже на трезвую голову, когда я думаю о подобном сравнении, при всем моем желании понять, почему Java в принципе может быть не то что быстрее, а хотя бы не медленнее C++, у меня не хватает аргументов даже для себя самого.
Для начала несколько “дано”:
Положим, A
– это линейная скорость выполнения машинного когда. B
– скорость компиляции байт-кода JVM в машинный код. Тогда общая скорость выполнения кода:
V(C++) = A1 + B1
V(Java) = A2 + B2
Очевидно, что B1 = 0
, так как С++ генерирует машинный код напрямую и не требует дополнительной работы в процессы выполнения. Но B2
стопроцентно НЕ ноль, так как каким бы эффективным не был компилятор JIT, он ВСЕГДА требует какого-то времени для компиляцию. Более того, JIT не компилирует все сразу, а “подкомпилирует” по мере прохождения путей выполнения. Получается, всегда есть ненулевая вероятность, что неожиданно придется выполнить код, ранее не требуемый, и потребуется время на его компиляцию. Даже если предположить, что компилятор JIT применяет изощренные способы предсказания путей выполнения и делает все, чтобы уменьшить B2
, но B2
по определению не 0. Если был бы 0, то не было бы JVM, а был бы чистый машинный код.
Далее, рассмотрим A1
и A2
. Эти параметры определяют, насколько эффективно компилятор создает код (или байт-код). По моему личному, субъективному и предвзятому мнению, у С++ (не С) больше шансов на оптимизацию благодаря шаблонам (компилятор имеет полноценную семантическую информацию для проведения inline’а) и генерация машинного кода под конкретную платформу (компилятор точно знает, какие машинные инструкции были бы максимально эффективны в каждом случае). Увы, я не особо силен в generic’ах Java, и руководствуюсь только слухами, что в Java они “ненастоящие”, добавленные гораздо позже и уступающие шаблонам C++. И так как компилятор обязан выдать стандартный переносимый JVM-код, то нет возможности оптимизировать под конкретную платформу. Есть надежда, что это сделает JIT, но там уже не будет семантической информации для более глубокой оптимизации. А еще JIT должен быть быстр, то есть будет компромисс между качеством оптимизации и скоростью компиляции. В С++ такой проблемы нет, так как компилировать можно как угодно долго.
Итак, это мои доводы для меня самого, измеренные в виртуальных попугаях. Не получается у меня убедить самого себя, что Java может быть быстрее или хотя бы на уровне с С++ по скорости. Буду рад за помощь в понимании этого вопроса.
Мы со Стасом проводили несколько несложных сравнений, в основном на реализации QuickSort, и Java по линейной скорости кода проигрывала где-то на 10%.
До C++ 2011 можно было говорить, у С++ нет модели памяти и стандартной библиотеки для потоков, поэтому у Java есть шанс выиграть на многопоточности, но сейчас у С++ все на месте. А подходы к многопоточности у С++ и Java, как мне кажется, одинаково неудобные (хотя std::async()
– это очень сильная возможность), и им обоим далеко до goroutines в Go, actor’ов в Scala и т.д.
Понятно, что 10% не всегда делают погоду. Иногда важнее развитые инструменты интроспекции, среды разработки, контролируемое выполнение, замена кода налету и много другое, что дает платформа Java, и не дает “молотилка” C++. Но зачем говорить про скорость то?
]]>
Если на просьбу написать несколько строчек кода человек начинает вместо этого уходить в объяснения – дело тухлое. Рыбак не может не любить держать в руках удочку.
Спасибо, Сергей, за интервью.
Сергей, первый вопрос не будет оригинальным, но это не делает его менее интересным. Расскажите, как судьба привела вас в мир компьютеров и программирования? Поделитесь вашей историей.
Сейчас забавно вспоминать, но компьютеры стали моей специальностью не случайно. Можно даже сказать, это было первое мое серьезное самостоятельное решение в жизни. :) В 1983 году я заканчивал выпускной класс физмат-школы-интерната (сейчас это физ-мат лицей при Киевском университете) и выбирал вуз и будущую профессию. Подошел к вопросу основательно: сначала сузил все, чем увлекался в детстве, до пяти тем. Это были электроника, программирование, химия, астрономия и, кажется, биология. Несколько месяцев осмысливал эти области, пока окончательно не остановился на программировании. И даже конкретнее: роботы. Поступил на физтех, и попал в самую точку. Правда, роботами заниматься так и не довелось, но это уже другая история.
Нельзя сказать, что на физтехе в то время как-то особенно хорошо учили программированию. Но там существовал кружок, неформальный клуб людей, очень увлеченных этим. Назывался “НСО” - научное студенческое общество. Немного, может быть человек пятнадцать на весь институт: от первокурсников до аспирантов. В среднем два-три человека с каждого курса. Народ регулярно собирался в общаге или в аудиториях, обсуждали всякие интересные вещи, старшекуры читали лекции для младших и т.п. Именно там я узнал про языки Лисп, Рефал, Си, систему Юникс. Участвовали мы и в студенческих олимпиадах по программированию. Физтех регулярно брал первое место в командном зачете.
Вы были одним из первых программистов, которые начали использовать UNIX в СССР. Что это был за UNIX и на каких машинах он работал?
Впервые я увидел живой Юникс в Курчатовском институте в 1986 году. В то время наша физтовская команда вместе с Курчатником занималась развитием системного софта для БЭСМ-6. В ИТМиВТ готовилась к выпуску новая машина этого ряда: Эльбрус-Б. Курчатовцы предложили не тащить на нее старую замшелую БЭСМовскую операционку, а сразу поставить Юникс. Нашей команде было предложено разработать систему программирования: Си-компилятор, ассемблер, загрузчик, библиотекарь и прочую мелочевку. Для меня потом это стало темой диплома.
В качестве инструментальной машины на первых порах использовалась СМ-1470. Довольно медленная машинка, со съемными 2.5-мегабайтными дисками. Но через год, в 1987 на физтехе появился учебный класс с машиной Labtam - классический юникс System V на процессоре NS32032. Это уже было кое-что. Я быстренько перенес туда экранный редактор RE из ОС Демос, к радости преподавателей, разобрался с русификацией терминалов (цветные графические, не хухры-мухры). И за это получил возможность использовать компьютер для своих разработок, в любое время кроме учебных занятий. Там и создавалась большая часть Си-компилятора для БЭСМ-6 и Эльбруса-Б.
В курчатнике тем временем народ перенес ОС Демос на “Электронику-85”, а также занялся разработкой версии Unix по заказу французской фирмы Utec - на процессоре Motorola 68000. Процесс шел успешно, система быстро стала стабильной, и одна из машинок Utec долгое время была нашей основной рабочей лошадкой. В первый раз я установил Emacs именно на нее.
Чуть позже, где-то в 1988 стали появляться персоналки с i286, на которых работал Xenix. Вполне юникс, но архитектура процессора ограничивала программные сегменты размером 64к. Мне в конце концов удалось собрать на нем gcc, но ценой перелопачивания приличной части кода. Зато с i386 уже никаких проблем не было. Главной системой сразу стал Interactive Unix, который Курчатник честно купил на Западе.
Юникс на БЭСМ-6 и Эльбрус-Б сделали в Новосибирском филиале ИТМиВТ. Мой компилятор пригодился. Но потом пришла волна писишек, и большие машины потеряли смысл.
У вас есть целый раздел в проектах - Ретрокомпьютинг. Видно, что вы целенаправленно собираете информацию о компьютерах прошлого и часто создаете их симуляторы, как программные, так и аппаратные. Что вас так привлекает в этом? В основной работе помогает?
Древние компьютеры - это же так интересно! :) Дело в том, что в них сохранилась и видна красота технических идей. Которую современные компьютеры, к сожалению, утеряли: стали слишком сложными и запутанными. Взляд назад помогает увидеть красоту в простоте, чего так не хватает в современных разработках. И в работе помогает, конечно: симуляторы и виртуальные машины сейчас самый передний фронт. Собственно, моя основная работа в MIPS - симуляторы.
Попробую задать провокационный вопрос. На моем личном опыте, особенно сейчас, когда сложность “железа” и программ увеличивается семимильными шагами с каждым днем, есть весьма четкое разделение между “железячниками” и программистами. Часто спецы по железу ужасно программируют, а программисты, в большинстве своем, не способны создавать “железо”. Но вы являетесь редким исключением. Как вы считаете – существует ли такая грань? Должен ли, например, спец по железу уметь написать алгоритм “min-cost-max-flow”, а программист спаять Спектрум?
Разделение существует, несомненно. Все известные мне серьезные инженеры четко делятся на электроников и программеров. Две разные наклонности: орудовать паяльником или возиться с формулами. Наверное, между ними заложено определенное противоречие. Структурное мышление и алгоритмическое. Строим или двигаемся. Но самое интересное происходит на стыке.
Странно слышать, что программистам на Java или PHP трудно дается понятие указателя. Наверное, им забыли рассказать про шины адреса и данных, счетчик команд, фрейм стека, прерывания и многое другое. Программисту необязательно уметь держать в руках паяльник, но читать схемы и разбираться в архитектуре процессоров, я считаю, важно. Как и для электроника разбираться в софте. Вряд ли можно спроектировать эффективную микропроцессорную схему, не зная как (а главное - зачем) в операционной системе происходит переключение процессов.
Все-таки, кто вы больше: программист или “железячник”? Когда есть выбор – каким типом проектов вам больше нравится заниматься?
Мне всегда было интересно происходящее на стыке. Паяльник был моим настольным инструментом лет с 12-ти. Простые схемы, от приемников до цветомузыки. Постоянно сканировал журнал Радио и другие в поисках интересных решений. Больше теоретически: нужные детали добыть было неоткуда. С литературой тоже напряг. В девятом классе моей настольной книжкой стал “Снобол-4”. Про другие языки тоже знал, конечно, но эта штука меня основательно увлекла. Все-таки я больше программист: алгоритмы и исходники мне ближе, чем паяльник и осциллограф. Вот чем симуляторы подкупают: вы получаете виртуальный хардвер по цене софтвера. :)
Можете назвать каких-нибудь известных программистов, которые для вас являются примером?
Да, конечно: главными учебниками для меня были идеи и исходные тексты великих мастеров. Деннис Ритчи и Кен Томпсон, Unix v6. Стивен Джонсон, компилятор pcc. Маршалл Кирк Маккузик, BSD Unix. Ричард Столлман, компилятор gcc. Эндрю Таненбаум, Minix. Это, так сказать, удаленные авторитеты, а еще коллеги и старшие товарищи, передавшие, так сказать, живой опыт: Сергей Рыжков, Алексей Руднев, Вадим Антонов, Сергей Аншуков, Валерий Бардин. Люди, создававшие ОС Демос и российский Интернет. Это были интереснейшие технологические задачки, с борьбой идей и бурной внутренней жизнью.
У вас в проектах есть несколько реализаций этюдов (язык ТРАК и игра Калах) из эпохальной (по крайне мере для меня) книги “Этюды для программистов” Чарльза Уэзерелла. Есть ли еще этюды, которые все еще хочется реализовать? Может взломать шифр Виженера?
Эту книжку я зачитал до дыр на первом-втором курсах физтеха. Она и сейчас стоит под рукой, на полке. Шифр Виженера взломал мой друг Леня Брухис. Он мне красочно рассказал всю историю в подробностях, что мое любопытство оказалось полностью удовлетворено. :) И задачку сжатия он тоже решил, так в NetBSD появилась утилита freeze.
Я сейчас перелистал - оказывается, остальные задачи я уже более-менее все решал, от бухгалтерии до компиляторов. Но по жизни появляется много других интересных идей, типа RetroBSD. Пока самая долгосрочная из них - создание RTOS для микроконтроллеров с защитой памяти, но без MMU.
Есть еще книги, про которые сильно повлияли на вас?
Snobol-4, как я уже говорил. Это был поворотный момент в осознании того, чем может быть компьютер. Интересно, что сейчас существует более современный язык того же автора - Icon.
“Операционная система Unix” Стива Баурна. После БЭСМ-6 и ЕС это был культурный шок: я не сразу поверил, что ОС может быть настолько простой и эффективной.
“Язык программирования Си” Кернигана и Ритчи. После Фортрана и Паскаля - ощущение свободы, как будто кандалы сняли.
“Структура и интерпретация компьютерных программ” Абельсона и Сассман - тогда еще в оригинале, ее только недавно перевели. Великая книга, с которой надо начинать всякое преподавание программирования.
Эндрю Таненбаум: “Операционные системы. Разработка и реализация” (Minix) и другие его книги. Это просто энциклопедия компьютерных знаний.
Когда появилась возможность заказывать книжки с amazon.com - примерно с 1996 года Сбербанк начал выдавать кредитки Visa - это было еще один глоток свободы. Сейчас уже много хороших переводных изданий появилось.
У вас еще еще одна страничка, где среди множества прочих интересных вещей, есть каталог “languages”, где вы пробуете различные языки. Как вы выбираете задачи, чтобы “пощупать” язык? И как вы, собственно, выбираете языки для изучения? Любимый язык есть?
Да, там я складываю небольшие примерчики, чтобы попробовать, “почувствовать” всякие интересные языки. Иногда попадается интересная задачка, из книжек или из ленты друзей в ЖЖ. Возникает идея сравнить ее на разных языках. Все языки интересны, а некоторые часто еще и имеют реальную практическую ценность. Например, D и Go - си-подобные компилирующие языки с автоматическим управлением памятью - удобнее чем Си++ и Java для большинства задач. Обдумываю применить Objective C для встроенных систем - он имеет прекрасную компонентную модель, достаточно поменять семантику runtime.
Языки, которыми обычно пользуюсь - Си и Питон. Избегаю и другим не рекомендую Си++ и Перл для реальных разработок, из чисто практических соображений. Посматриваю на Хаскель и Эрланг, как источник идей на будущее. Все языки интересны, и радует, что развитие не останавливается.
Уверен, вы слышали про Raspberry Pi. Можете прокомментировать как человек, знающий процесс изнутри – реально ли создать подобное устройство за те деньги, за которые его продают? Или это убыточный проект? Просто если это не так, почему такие проекты не являются массовыми?
Цена примерно соответствует себестоимости. В целом это хороший маркетинговый проект Broadcom и ARM, с пользой для социума. Они не массовые, потому что отсутствует коммерческая составляющая, и вокруг не образуется эко-система из фирм, развивающих и поддерживающих проект. В отличие от Ардуино, например.
На каком языке стоит начинать учиться программированию? Есть подходы, например, используя диалекты Лиспа, когда люди учатся программировать, практически не тратя времени на изучение самого языка (синтаксис Лиспа объясняется за пару минут).
Здесь у меня нет однозначного мнения. Я пробовал давать своим сыновьям и Лого, и варианты Лиспа, но лучший результат получается, все же, с Паскалем. Си и Джава хорошо идут позже, в студенческом возрасте. Для школьников, возможно, Питон был бы лучшим выбором.
Императивное и функциональное. В мире современного “железа” есть ли применение для функционального подхода? Оправдано ли оно, или это дань моде?
Сейчас это активная область исследований. Например, Bluespec - новый язык для разработки хардвера вместо или вместе с SystemVerilog - разрабатывался как расширение Haskell. Сложность хардвера растет, и повышение уровня абстракций может сильно помочь - если не будет идти во вред таймингу. Полезность надо еще подтвердить практикой, но надежда всегда есть.
Разработка и тестирование. Расскажите, как вы тестируете (на работе или дома) “железячные” проекты? Применимо ли TDD (test driven development) в разработке аппаратуры? Как автоматизируется тестирование аппаратуры?
Всякие домашние проекты я обычно тестирую кое-как: хобби оно и есть хобби. Даже название придумал: тяп ляп инжиниринг лтд. Но на работе - совсем другое дело. Поработав в MIPS, теперь я понимаю, как надо вести разработку: это действительно test driven development, ровный процесс с выходом ожидаемого качества. Ценный опыт. Но очень высокие требования к составу и уровню команды.
Разработка системы на кристалле происходит примерно так. Архитектор разрабатывает внешнюю спецификацию и архитектуру. Это постоянный процесс: к концу разработки подробно известно, что происходит в каждом блоке системы на каждом такте. Инженеры-электроники собственно проектируют систему, создавая синтезируемый код на Верилоге. Группа тестирования дополняет его до полной системы, создавая RTL-симулятор. Отдельно программисты делают функциональный симулятор системы, исходя из внешней спецификации. Параллельно делаются подробные тесты для каждого элемента или функции системы, в соответствии с архитектурой. Тесты гоняются на RTL-симуляторе и на софтверном симуляторе, и проверяется их идентичность. Ошибки означают расхождение или в аппаратуре, или в архитектуре. Процесс разработки и тестирования итеративный, и когда он завершается, заказчик получает: описание архитектуры и ее программный симулятор, функционально полный хардвер и точный его симулятор, плюс огромный набор верификационных тестов. Хотите что-то доработать? Нет проблем: вносите изменения, прогоняйте тесты, чините сломавшиеся места. Очень технологично.
На чем вы работаете? Например дома. Судя по проектам, вы используете и Windows, и Linux, и Mac.
На работе Linux на столе и большинстве серверов, дома Mac-mini и Windows на виртуальной машине. Ноутбуки я постепенно перестал уважать, iPad закрыл все потребности. Раньше основной домашней машиной был Линукс, но приходилось при каждом серьезном обновлении системы решать проблемы с драйверами. То звук отвалится, то USB, то видео. С переходом на Мак жизнь упростилась.
Кстати, вы “социальный” программист? У вас есть G+ и Facebook, Твиттера вроде нет. Какие блоги вы читаете? И еще более интересно – сколько? Ведь время всегда ограничено, и как-то приходится выбирать между проектами и работой, и чтением новостей.
Лента друзей в ЖЖ и фейсбуке для меня главные источники информации о мире. G+ пока не прижился, еще менее удобный, чем ФБ. С утра, как проснусь, хватаю iPad и читаю все новости за ночь. :) И днем тоже частенько отвлекаюсь от работы на ЖЖ, в качестве перекура. Пишу и комментирую реже, когда находится что-то интересное. Еще активно использую Google Reader, собираю в кучу RSS-ленты разных газет, новостевых сайтов, elementy.ru, anekdot.ru, dirty.ru - сейчас практически все поддерживают эту фишку.
Если вы вам надо было бы нанять толкового программиста – какие бы вопросы вы задавали?
Какими своими проектами вы гордитесь? Какой проект вы бы хотели сделать? Ну и код полистать, конечно.
Мне приходилось интервьюировать программистов. Если на просьбу написать несколько строчек кода человек начинает вместо этого уходить в объяснения - дело тухлое. Рыбак не может не любить держать в руках удочку.
Сложно было найти работу в США? И в моральном, и техническом смысле? Много ли русско-говорящих в MIPS?
Нельзя сказать, что я ее целенаправленно искал, просто оказия подвернулась (и даже две сразу). Мне очень помог Юра Панчул, с которым мы знакомы еще со времен учебы на физтехе. Он меня давно звал, уже лет пятнадцать, но в этот раз звёзды правильно встали. Насколько я вижу, найти работу несложно - при определенном уровне квалификации. Здесь точно так же не хватает специалистов, как и в России. А поскольку MIPS начинает строить планы сотрудничества с российскими разработчиками, пополнение команды русскими инженерами приветствуется.
В моральном плане переезжать нет никаких проблем: все тебя поддерживают и одобряют. Труднее было пережить развал, останов и прекращение всех дел, в которые я был включен в Москве, но это происходило независимо и до решения о переезде. Как в известном анекдоте: говорят, у нас кризиса не будет. Как не будет, только что ведь уже не было?
В техническом плане пришлось поднапрячься, чтобы уладить все дела, передать жилье и заботы по хозяйству на старших детей. Они хоть уже взрослые и живут самостоятельно, все равно волнительно.
Русскоговорящих в MIPS шесть человек, включая меня. Из них четверо из exUSSR и двое американцев, изучавших русский язык. Плюс много старых знакомых из Курчатника и ИТМиВТ, плюс новые знакомства - по родной речи скучать не приходится.
Ну и под занавес, вы сам выбрали профессию, или она таки сама выбрала вас?
Я выбрал, однозначно. Но мне очень повезло с людьми, с которыми сводила судьба. Вообще, в каждом человеке заложено зерно, которое стремится прорасти. Важно попасть на правильную почву. Тут мне грех жаловаться.
Сергей, спасибо за интервью. Ждем ваших новых проектов. Удачи.
Большое спасибо за интересные вопросы.
Всего хорошего,
Сергей
// Сергей Вакуленко, Александр Дёмин // Июнь 2012.
]]>Disclaimer: Этот пост ни как не связан ни с компанией Splunk, ни c моим работодателем. Все это мое личное мнение о продукте на основе свободной информации с сайта Splunk и посещения открытой бесплатной конференции SplunkLive 2012.
Грамотные логи – это практически все, ну или почти все. Если логирование сделано по уму, то и разработчикам будет легче, а службе поддержки еще легче.
“Ну а теперь – два слова о себе!”. Мы делаем банковский софт, серверная часть которого – это долгоиграющие неинтерактивные процессы. Логи (и иногда coredump’ы) – это единственный способ их мониторить. С некоторого времени мы используем Google Log в качестве механизма ведения логов с некоторой небольшой самодельной прослойкой сверху для интерфейса к C и прикладному уровню (у нас собственный язык для написания бизнес логики, типа как у 1C).
Итак, логи ведутся. Прикладной уровень тоже все больше и больше служебной информации пишет в них, что отлично. Более того, логи в виде текстовых файлов – это удобно (даже на Windows, и не говорите мне про недоразумение, называемое Windows Events).
Но вот удобства заканчиваются, и начинаются проблемы. Для начала, при многосерверной распределенной системе, чтобы централизировано анализировать логи, их надо выуживать с разных машин. Кроме того, разные подсистемы в силу разных, часто исторических причин, пишут информацию в совершенно разных форматах. И как прикажете их анализировать? хардкодить? Изобретать всякие rule engine’ы? А клиенты (читай, банки), хотят красивые, удобные и наглядные dashboard’ы, конфигурируемые системы оповещения, триггеры, интеграцию с другими продуктами, возможность горизонтально расширятся (не знаю, как по-русски сказать “to scale horizontally”) и т.д. Как я уже говорил, мы делаем банковский софт, делаем его хорошо, и совершенно не хотим тратить силы на побочные продукты, связанные с инфраструктурой. Было несколько попыток создать собственную систему мониторинга, но, увы, для банков уровня Tier 1 – это не та задача, которая может быть решена одним пальцем левой ноги. Вывод: нужно хорошее third-party решение, причем которое будет работать одинаково на UNIX’ах и Windows.
Надеюсь я достаточно нагнал страху. К делу.
Был обнаружен продукт, называемый Splunk. Удивительно, но суть этого продукта можно описать одним абзацем. А если вообще одним предложением, то это развитая система анализа неформатных текстовых данных, чем логи и являются. Входными данными являются произвольные текстовые данные, разбитые на строки. Способы их доставки в Splunk весьма гибкие. Самое простое – установить на сервера агент Splunk, указать ему каталоги или конкретные файлы, которые надо мониторить, и он будет автоматически отсылать обновления на центральный сервер. Можно самостоятельно доставлять логи на сервер Splunk, например, посылая их туда через по HTTP, TCP, UDP. Еще Splunk умеет брать данные из Windows (Logs, Events, Performance Counters). В общем, путей много. Мы остановились на агентах.
Итак, сервер Splunk постоянно получает актуальные обновления. Он как-то их сам хранит и индексирует. Есть возможность развернуть кластер Splunk-машин, и они будут распределено использоваться для хранения данных и поиска.
Но самое интересное начинается дальше. У Splunk есть специальный язык запросов, предназначенный для анализа неструктурированной текстовой информации. Основной принцип – это разнообразные фильтры, применяемые построчно к указанному куску данных, и результаты данных можно через pipe (|
) как в unix’е “нанизывать” один на другой.
Например:
sourcetype=access_* | redup by user_id | transaction by tran_id | sort by duration desc | head 10
Берем файлы, начинающиеся с access_
, затем “сжимаем” повторяющиеся записи по критерию повторяющегося значения поля user_id
, затем группирует записи по полю tran_id
(фактически логически “сжимаем” строки с одинаковыми значениям поля tran_id
в транзакции), затем сортируем по длительности (поле duration
автоматически добавляется фильтром transaction
) и отбираем первые десять сверху. Итак, в итоге мне имеем десять самых длинных транзакций от уникальных пользователей. А всего то, написали один запрос.
Откуда берутся так называемые поля? Поля вычленяются из строк данных. По умолчанию разделителем полей является пробел, для разделения имени поля и значения является “равно”. Но все это, конечно, конфигурируемо. Фактически, любой формат, который можно разобрать программно, можно настроить. Специальными фильтрами можно создавать вычисляемые поля, причем ссылать можно не только на данных из текущей строки, но и на другие строки, отобранные по какому-то критерию. Все очень похоже на SQL, только источником данных являются не таблицы в базе данных, а тестовые строки. На выходе – табличные данные, которые можно трансформировать в новые таблицы (точнее, представления, view) и т.д. Результат можно оформить в виде красивой таблички или графической диаграммы. Да, чуть не забыл. Можно делать подстановки полей на основе запросов во внешние источники, например, RDBMS или RESTful-сервис. Все налету.
После того, как запрос отлажен, его можно запомнить и добавить в dashboard. Настроенный dashboard можно оформить в виде так называемого приложения (фактически, это набор xml-файлов) и, например, передать клиенту. На сайте Splunk’а есть даже магазин приложений (хотя большинство из них бесплатны). Можно, например, взять готовое приложение для анализа логов Microsoft Exchange’а, воткнуть и начать сразу получать удовольствие.
Путей для визуализации результатов поиска много: таблицы, графики, выгрузка во внешние источники, даже интеграция с Google Maps. Я видел живое демо, где человек из логов веб сервера вытащил информацию о положении пользователей, отсортировал и отфильтровал по вкусу, и в конце представил в виде Google Maps виджета, где было видно, откуда были запросы. И все это через несложные Splunk-запросы. Тот пример, что привел я – это микрочастичка того, что можно сделать запросами. Вкратце можно ознакомится со списком команд, так, чтобы понимать примерно возможности.
По-хорошему, имея Splunk и логи веб сервера, такие вещи как Google Analytics могут больше не понадобится. Splunk говорит, что многие их большие клиенты, кто работает c веб, так и делает, так как можно автоматически привязывать аналитику к любым внутренними данным, идущих от разных источников.
Если так задуматься, то если использовать Splunk, то логирование само по себе упрощается. Например, не надо придумывать, как именовать логи. Системе логирования достаточно использовать время в имени и порядковый номер, если лог был обрезан по длине. Но все это только ради уникальности файла в файловой системе. После “скормления” файла в Splunk имя будет уже не важно. Далее, можно не париться в вычислении длительности событий (это бывает удобно для профилирования). Это можно сделать через вычисляемые поля в запросе.
Важно, что для информации, проиндексированной Splunk’ом, нет различия в доступности в зависимости от ее возраста. Все данные доступны для запросов с одинаковой скоростью, ограниченной производительностью только вашего сервера или серверов, где развернут Splunk.
Идем далее. Как можно продукт попробовать? Бесплатно скачать с официального сайта (требуется свободная регистрация). У Splunk следующая ценовая политика: цена продукта определяется только объемом данных, прогоняемых через систему в день. Если ниже какого-то минимального порога, то можно использовать бесплатно.
У меня время от скачивания дистрибутива под Windows, установкой, подсосом локальных логов с той же машины (пока без агентов) и началом игры с запросами по ним было около получаса.
]]>За последнее время удалось пощупать еще несколько мини машин, правда несколько иного класса, чем Maximite, построенных на ARMах.
Сразу извиняюсь за качество фоток, но нормально фотографировать яркий экран у меня не получилось ни на айфон, ни на мыльницу.
Этот я купил сам. Цена вопроса - 70$ + доставка. На официальном сайте уже продается более современная модель с гигом памяти. У меня только 512МБ.
Мои фотки:
USB номер один. Через него можно также питать, но говорят, что требуется два ампера, так что просто от компьютера может не получится. Я через этот разъем подключал клавиатуру.
Mini HDMI. Через переходник также получилось подключить к DVI-монитору.
На торце еще USB и внешнее питание.
Разъем микро-SD. По размеру весь корпус чуть больше флешки обычного дизайна.
Подключил к телеку. Андроид вшит, время загрузки до этой заставки ~5 секунд.
Время полной загрузки ~25 секунд.
Подключил мышь, кое-как на виртуальной клавиатуре ввел пароль WiFi и сразу зашел на правильный сайт.
Десктоп ориентирован на использование как медиа-центра, поэтому все такое громадное на экране. Немного поёрзав со шрифтами, получил такое.
Теперь еще один правильный сайт.
Параметры внутреннего хранилища.
Что за андроид.
Предустановленные приложения.
Под занавес еще один правильный сайт. Увы, JSLinux не смог запуститься. Наверное, дело в браузере.
Вшитый андроид работает флешки не требует. Если подготовить флешку с другим линуксом, то он с нее загрузится. А попробовал образы Ubuntu 12.04 и Lubuntu с официального сайта. Увы, реально все шевелится очень неотзывчиво. Нужен нормальный дистрибутив, оптимизированный под эту платформу.
Можно поглядеть еще картинки на официальном форуме. Также есть процедура разбора.
Вывод: не совсем ясно, для чего нужно такое устройство. С Maximite можно хоть на аппаратном уровне поиграться. А тут: разве что запустить XBMC и использовать машину подобного рода как медиа-центр.
Производится SolidRun. Этот девайс купил коллега, уставший ждать Raspberry PI (я свой тоже еще не получил), и дал мне поиграться.
Тут процессор немного послабее, но больше периферии (хотя нет WiFi), и тоже обещают 1080p видео. Этот больше тянет на роль медиа-центра.
Представляет собой кубик, со стороной примерно как спичечный коробок.
Питание, сеть, HDMI, micro-SD, два USB, eSATA.
SPDIF, micro-USB (консоль).
Девайс выполнен как-то кустарно. Корпус, судя по второй картинке, точили явно руками, и не очень прямо. Но в целом – работает отлично. На борту Ubuntu 10.04.
Этот девайс мне дал другой коллега, таки дождавшийся заветной платки. Она продается как есть, без блока питания и флешки для загрузки. Надо самому ее готовить, хотя информации в сети полно.
Из интересного: у PI есть порты-пины общего назначения (GPIO) (на фотке в левом верхнем углу), к которым можно теоретически подключать всякие самоделки. Вообще, PI задумывался как образовательное устройство, а не конечное.
Какой-то прям бум наблюдается с подобными миниатюрными компьютерами. Растут как грибы. И ARM - это здорово. Одна с ними пока засада – Хром на них не работает по понятным причинам.
]]>Итак:
#include <future> #include <vector> #include <deque> #include <list> #include <forward_list> #include <typeinfo> #include <iterator> #include <iostream> template <typename T> void go(T f) { auto start = std::chrono::high_resolution_clock::now(); f(); auto stop = std::chrono::high_resolution_clock::now(); auto duration = std::chrono::duration<double>(stop - start).count(); std::cout << duration << "\n"; } void erase(std::forward_list<int>& c, std::forward_list<int>::iterator i) { c.erase_after(i); } template <typename T> void erase(T& c, typename T::iterator i) { c.erase(i); } template <typename T> void test() { std::cout << typeid(T).name() << "\n"; size_t const N = 100000; T v(N, 0); std::srand(0); for (auto t = N; t > 0; --t) { auto i = v.begin(); std::advance(i, std::rand() % t); erase(v, i); } } int main() { go (test<std::vector<int>>); go (test<std::list<int>>); go (test<std::forward_list<int>>); go (test<std::deque<int>>); }
Перегруженная функция erase
сделана для единоообразия функции test
.
Компилируем в VS11:
cl /O2 /EHsc test.cpp && test
Результат:
class std::vector<int,class std::allocator<int> >
1.40678
class std::list<int,class std::allocator<int> >
8.85827
class std::forward_list<int,class std::allocator<int> >
8.70124
class std::deque<int,class std::allocator<int> >
9.19784
Разница, конечно, не в разы, но и не совсем уже незаметная, как для list
и forward_list
.
Начну с цитаты из этой книги:
I don’t believe anything really revolutionary has ever been invented by committee.
Каково!? Только ради этой цитаты книгу стоит прочитать.
Биография Стива Джобса была интересным чтивом, хотя и весьма длинным. Книга Стива Возняка гораздо короче. И гораздо ближе читателям с инженерными интересами.
Стив Возняк рассказывает, как все произошло. Как он школьными ночами на бумаге проектировал, перепроектировал, снова и снова перепроектировал несуществующие пока компьютеры. Как ему это помогло развить удивительный навык всегда использовать минимальный возможный набор микросхем, а когда надо - подключать программную часть. Примером может служить первый контроллер гибкого диска для старших версий Эппл 2. Вместо сложного и дорого аппаратного решения, реализующего полный протокол обмена с приводом, он поставил всего несколько чипов, а остальное реализовал программно на ассемблере 6502, виртуозно выполнив требования к длительности сигналов. В дополнение собрал еще на нескольких чипах state-машину, программу для которой написал на самим же придуманном “птичьем” четырех битном машинном коде (эдакая самопальная перепрограммируемая логика). В итоге, в какой-то момент Стив в одиночку создал Эппл 2, революционный персональный компьютер с цветным дисплеем-телевизором и полноценной клавиатурой. Кстати, вопреки расхожему мнению, Джобс вообще не участвовал в самой разработке. Через несколько лет эта машина сделает Возняка мультимиллионером и запустит компанию Эппл. Неудивительно, этот компьютер обошел по простоте, функционалу и цене всё, что выпускалось на тот момент корпорациями.
Но история на этом не кончается. После этого Стив попадет в авиакатастрофу, управляя небольшим самолетом и некоторое время будет страдать амнезией. Потом уход из Эппл, и опять, вопреки расхожему мнению, не потому, что они там все переругались, а просто он захотел реализовать идею универсального программируемого пульта дистанционного управления. Формально, он никогда не увольнялся из Эппл и по сей день даже какую-то заплату там получает.
История и на этом не заканчивается. Стив почему-то решил организовать гигантский рок-фестиваль. В итоге их было несколько. И тут не обошлось без технологических инноваций. На фестивале впервые был организовал спутниковый телемост с СССР (использовалось оборудование, установленное во время олимпиады в Москве), прямо через все еще существующий “железный занавес”.
После этого Стив около десяти лет преподавал основы компьютеров школьникам и занимался благотворительностью.
Целый пласт книги также посвящен шуткам, которые Стив реализовывал самостоятельно на протяжении всех этих лет (с применением технологий и без). Например, сервис анекдотов по телефону, который Стив организовал у тебя дома. Или портативный источник помех для телевизора, которым он донимал соседей по общежитию в колледже. Или целая эпопея по роспуску слухов на конференции, где выставлялись продукты Эппл, о некой компании Золтéйр, также якобы принимающий участие в конференции и имеющей крайне мощный продукт. Даже Джобс купился на это и узнал о подставе многими годами позже.
Итак, если у вас есть хоть какой-то интерес к биографиям, то эта книга отличное чтиво про настоящего гения компьютерной индустрии.
]]>Вот пример мыслей в процессе подготовки релиза собственного продукта: “Блин, вот тут косячок, тут проблемка, тут потенциально может вылезти что-нибудь, тут немного недоделано, тут опечаточка в документации” и т.д. Так как ты знаешь всю подноготную, то часто переоцениваешь проблемы.
Когда же ты берешь красивый весь такой запакованный архив чужого продукта, думаешь – вот, блин, как у них все классно сделано! Хотя, понятно, что этот продукт кто-то паковал со схожими мыслями, и просто тебе со стороны никогда не обнаружить маленькие и не очень проблемки, которые там, конечно, есть.
По роду текущей работы я вовлечен в процесс релизов, сортировки багрепортов, разбора полетов и т.д. Я обычно делаю так: продажи растут? Да. Количество жалоб на последний релиз со стороны клиентов уменьшилось? Да. Аналогично для внутренних пользователей? Да. Вывод: все в норме. А остальное – нормальный процесс разработки со своими особенностями.
]]>Далее, внизу каждого поста есть ссылка “Disclaimer”, чтобы не было недопонимания. Я обычно ссылаюсь на книги, которые читал или рецензировал сам, и их рекомендация ни как не связана издателями или автором.
Информация, размещенная на коммерческой основе, будет явно указана прямо в теле поста со словом “Реклама”.
Есть желание обсудить? Прошу в комментарии.
Если у вас есть класс, для которого, как вам кажется, надо реализовать конструктор копирования или оператор присваивания, что-то вы делаете явно не так. // Питер Соммерлад
Сегодня в гостях у моего блога Питер Соммерлад. Мне довелось недавно побывать на его выступлении на ежегодной конференции ACCU в Оксфорде. Питер давно занимается C++, и более того, принимает участие в работе над стандартом C++. Будучи профессором университета HSR (Рапперсвиль, Швейцария), вместе со своими студентами и аспирантами он работает над разнообразными библиотеками и средствам разработки для С++ и ряда других языков под Eclipse.
У нас будет возможность задать Питеру вопросы, которые не являются частью его выступлений или публикаций.
У меня были очень хорошие оценки по математике в школе, и в выпускном году был дополнительный курс, где надо было работать с программируемым калькулятором HP 33, в основанного на стеке операндов. В отличие от своих друзей, у которых уже были калькуляторы Casio и TI-59, или даже домашний компьютер Commodore V(I)C 20, я ничего не знал о программировании. Однако, благодаря своему учителю, мне удалось портировать игру “lunar lander”, о которой я прочитал в журнале, с основанного на регистрах калькулятора TI-59 на школьный HP 33. В итоге у меня получилась баллистическая “стрелялка”, где надо было с определенного расстояния поразить цель, загороженную препятствиями. Эдакие Angry Birds восьмидесятых для карманного калькулятора с семисегментным жидкокристаллическим экраном в десять разрядов. Требовалось определенное воображение, чтобы понять эту игру: вводится два числа, угол и скорость, и программа сообщает, попали ли вы в препятствие или насколько было отклонение от цели. Такая вот была моя первая программа.
Тот курс программирования для HP 33 и также идея старшей сестры, знавшая людей, изучавших информатику и при этом зарабатывающих неплохие деньги, будучи студентами, подтолкнули меня записаться на курс математики и начать изучать информатику во Франкфурте. Перед началом учебы я получил свой первый домашний, “настоящий” компьютер, Sinclair ZX Spectrum, c невообразимым тогда объемом памяти в 48Кб и кассетным магнитофоном. Тут уже можно было программировать на Бейсике, что означало, как говорил наш учитель математики, “компьютер мог сам распределять память под переменные, и не надо было помнить, что ты поместил в регистры R1 и R2”. Одной из моих первых программ для ZX Spectrum была отрисовка фигуры в форме сердца (да, “заливка” цветом подобной области была непростой задачей). Я показал это подруге, но, честно говоря, не уверен, что она оценила это, так как я начал уделять компьютеру больше времени, чем ей. Мы женаты уже более двадцати лет, но иногда я напоминаю ей эту историю. :-)
Затем пришло время университета, и в первом семестре мне пришлось научиться программировать на ассемблере для Univac 1100. После лекции про косвенную адресацию, мы с другом написали программку из двух команд, которая в бесконечном цикле загружала свои собственные команды. Когда мы ее запустили, через 2 секунды получили “watchdog error”, что на мейнфрейме означает, что все остальные программы были остановлены на две секунды. После трех таких запусков, в зал “влетел” оператор из серверной и начал выяснять, кто работает под нашим с товарищем логином. Мы медленно подняли руки. Он препроводил нас к себе в кабинет, и после расспросов как и что мы делали, таки отпустил нас с миром, взяв обещание не рассказывать другим студентам про этот “прием”. Во втором семестре начался Паскаль и “настоящие” программы. Указатели в Паскале были реальной задачей, так как они появились без какого-либо объяснения, для чего они нужны, или примера, типа связанного списка. Только после этого я смог понять их. Через нескольких семестров я начал подрабатывать в небольшой фирме, созданной старшими сокурсниками. Там у меня была возможность реально применять полученные знания и набираться опыта на реальным задачах и языках (dBaseII, Dataflex, UCSD Pascal и Microsoft Pascal). Например, на Microsoft Pascal под DOS я написал библиотеку для создания баз данных, основанной на B*-дереве.
Очень большое влияние, я полагаю, на меня оказала возможность во время диплома работать с исходными текстами UNIX, и особенно утилит из нее, таких как make, awk и т.д. Я написал фронтенд для Modula-2 на С с использованием lex и yacc. Чтобы иметь возможность работать из дома, мне пришлось портировать все необходимые утилиты на шестнадцатибитный компьютер с DOS. На было разобраться, как все работает изнутри, и работы Денниса Ритчи и его коллег были отличным примером. Возможно, оттуда берет начало моя любовь к простому и элегантному коду.
Одной из первых книг, что я помню, были “Pascal User Manual and Report”, Kathleen Jensen и Niklaus Wirth, и “Algorithms and Data Structures = Programs”, Niklaus Wirth. Кстати, в последней было много ошибок, если слепо набивать примеры. Например, в той редакции, что была у меня, сортировка слиянием была нестабильной, вопреки сути этого алгоритма.
В целом, было много книг, повлиявших на мои взгляды в программировании, и непросто выбрать что-то конкретное. Могу рассказать про книгу, где я являюсь соавтором: “Pattern-oriented Software Architecture: A System of Patterns”. В процессе ее написания, я очень многое узнал о программном обеспечении в целом, его архитектуру и работу в команде. Уже после публикации, выступая на конференциях и рассказывая о книге, я познакомился со множеством интересных людей, которые без сомнения повлияли мои взгляды. С некоторыми мы стали друзьями.
Сложный вопрос. Я имел в виду C++11 и множество приемов, которые люди использовали раньше, и которые более не нужны, если вы не разрабатываете библиотеки. Но лучше привести пример.
В книге Страуструпа “The C++ Programming Language” в качестве примера создания класса рассказывается про класс Vector, и как там происходит управление памятью через указатели. Проблема в том, что правильная и безопасная с точки зрения исключений реализация такого класса является очень сложной даже для экспертов C++. Рядовой же читатель, решив сделать подобный класс или, например, класс для строчек, скорее всего сделает это неверно. Более того, делать это совершенно не нужно.
Это похоже на ситуацию с книгой Вирта про алгоритмы и структуры данных. “Все” студенты, кому приходилось ее изучать, хотят реализовать свой связный список просто потому, что он им “все равно нужен”. Может на C это и имело смысл, но в библиотеках современных языков (не только в С++) есть множество хороших и проверенных структур данных. Я был мог “порассуждать” про качество библиотек в Java, но я оставлю это моему коллеге, преподающему алгоритмы и структуры данных в Java в нашем университете.
Сейчас, после того как мы поняли, чего делать не надо, я бы рекомендовал просто использовать стандартную библиотеку C++, особенно алгоритмы, так как с помощью них даже циклы, которым нас учили, можно не использовать. В С++11 нам более не требуется управлять ресурсами вручную. Можно просто использовать std::vector, std::string, std::shared_ptr/std::unique_ptr и т.д., и компилятор сам создаст правильные конструкторы и деструкторы. Если у вас есть класс, для которого, как вам кажется, надо реализовать конструктор копирования или оператор присваивания, что-то вы делаете явно не так. Кстати, с boost это также почти всегда верно и для C++03.
Так что, до тех пор, пока вы глубоко не изучили приемы создания библиотек, просто используйте стандартные и не изобретайте своих. И даже после этого, избегайте “ручного” управления ресурсами. Чем больше я погружался в проблему создания надежным переносимых стандартных библиотек, тем больше проникался уважением к тем, кто имеет навыки и терпение для этого. Это реально сложно. А большинство прикладных задач, решаемых на С++, не требуют подобных знаний, если вы используете стандартную библиотеку. Хотя, ничто не мешает вам стать разработчиком библиотек.
Мне не очень много удалось сделать для C++ 2011, так как я весьма поздно подключился к проекту. Однако, мне удалось получить финансирование от нашей школы (спасибо директору, профессору Херману Меттлеру) для проведения собрания комитета стандартизации C++ в Рапперсвиле в Августе 2010-го. Несколько предложений в разделе про std::async были переписаны мной при поддержке многих других членов комитета. В процессе написания книги “Simple C++” я часто ссылаюсь на стандарт и периодически нахожу незначительные ошибки, которые будут исправлены в следующей редакции.
Что значит “не нравится”? Я могу принять стандарт как есть, так как я знаю, чего стоило его создать. Все недостатки, которые могли бы меня касаться, будут исправлены в следующей редакции, и я работаю над этим. Порой вы должны идти на компромисс с комитетом.
Если б я, будучи вовлеченным в процесс раньше, предложил что-то, и мое предложение было бы отклонено другими членам, я был расстроился. Размышляя на эту тему, я бы сказал, мне не нравится, что const разрешено ставить слева, тогда как логика подсказывает, что это должно быть справа. Оба варианта синтаксически одинаковы. Я спрашивал, что будет, если я сделаю подобное предложение. Скорее всего оно будет отклонено “старшими товарищами”.
Для начала, библиотека состоит только из заголовочных файлов, и поэтому не надо думать о линковке. Это особенно важно для моих студентов. Я написал статью про CUTE в журнале “ACCU Overload #75” (слегка измененный вариант статьи можно скачать с http://wiki.hsr.ch/PeterSommerlad/wiki.cgi?CuTe). Главная причина в том, что меня сильно не устраивала библиотека CppUnit и также другая unit-тест библиотека, разработанная мной ранее в девяностых. Вдохновленный Кевлином Хенни я попробовал использовать стандартную библиотеку и минимизировать использование макросов, даже если это означало, что тесты должны быть вручную “зарегистрированы”, а не автоматически, используя статическую инициализацию (например, GoogleTest использует для этого макросы). Я хотел избежать статической инициализации, так как я уже имел с ней проблемы в прошлом при использовании разделяемых библиотек и их зависимостей. Проблема регистрации тестов решается нашим плагином CUTE для Eclipse CDT (http://cute-test.com). Плагин сгенерирует необходимый код для регистрации теста и также проверит на случай, если вы забыли сделать это вручную. Конечно удобно, когда есть возможность просматривать различия, регистрируемые через ASSERT_EQUAL(), но это требует возможность печати значений в поток std::ostream&. Это является проблемой для разработчиков встраиваемых систем, если они хотят запускать тесты на “железе”, так как из-за ограничений по размеру кода не удается использовать iostream. Однако, у меня в планах выпустить в этом году версию CUTE, где можно будет конфигурировать использование iostream.
Двое моих студентов реализовали поддержку TDD в Eclipse CDT, а один из аспирантов довел до состояния законченного продукта. Например, наш модуль TDD может автоматически создавать объявления переменных, которые используются в коде, но еще не объявлены. Аналогично для новых необъявленных классов, перечислений (enums), свободных функций или членов классов. Это очень удобно вне зависимости от стиля разработки: TDD или сверху вниз. Скоро в плагине CUTE будет добавлена еще одна возможность: Mockator. Mockator генерирует код и фрагменты конфигурации Eclipse CDT для применения техники “dependency injection” в уже существующем коде на С или С++ с использованием так называемых швов. Идея швов описывается в отличной книге Майкла Физерса “Эффективная работа с унаследованным кодом” (Michael Feathers, “Working Effectively with Legacy Code”). По этой методике Mockator может создавать шаблоны тестов и mock’и для функций и типов. Про Mockator можно прочитать в выпуске 108 журнала “ACCU Overload”.
Я использую vi с 1985 года, и все еще создаю в нем небольшие файлы, разнообразные одноразовые скрипты. Мне нравится, что vi есть везде.
Однако, для серьезного программирования хорошая автоматизированные среда (IDE) просто необходима. Это как ехать на сотни миль на мощной машине или на детском трехколесном велосипеде. Конечно, есть люди, ездящие на детском велике очень быстро, но на машине просто в разы удобнее. Наверное, аналогия с машиной не самая удачная, и вертолет лучше бы тут подошел, так как хорошая среда разработки (например, Eclipse CDT) позволяет быстро перемещаться по коду, поэтому можно очень легко разбираться, даже если его очень много. Чтобы перейди от объявления к определению достаточно навести мышку или кликнуть, при этом среда помнит, где вы были, и можно быстро вернуться. Любой программист на Java, использующий Eclipse, наверное захочет использовать другой язык, только если для него будут похожие инструменты. Мы стараемся сделать Eclipse CDT таким же удобным для C++, как и для Java. Это непросто, и мы рады любому спонсорству.
В нашей университетской программе мы всегда даем студентам очень много лабораторных работ в курсах программирования. Как минимум 3-4 больших проекта являются частью диплома бакалавра. Выполнение лаб нетривиальным образом реально помогает студентам. Например, я часто даю упражнения на С++, которые надо выполнять без циклов или рекурсии, а только используя алгоритмы из STL. Решение с циклом может быть очевидным, но найти красивый алгоритм несравнимо полезнее.
Увы, некоторые соревнования по программированию ориентированы исключительно на скорость, а не на качество кода в далекой перспективе. Поэтому я всегда, а не только на лабах, но и на экзаменах прошу студентов писать unit-тесты.
Для C++11 такие примеры найти пока непросто, так как компиляторы появились только недавно. В дополнение к этому, много примеров на C++11 с новыми возможностями языка, например, семантика перемещения, которые не объясняют где и как стоит применять эти возможности. Надо помнить, что многие реализации стандартной библиотеки гораздо сложнее, чем код, с которым вам хотелось бы работать, так как приходится иметь дело с макросами, обеспечивать обратную совместимость и разбираться с недостатками компиляторов. Решение подобных проблем не для новичка, и к тому же дает неверное представление о том, как надо писать код. Если честно, даже в CUTE, так как я пытался поддержать C++03/boost, std::tr1 и C++11, есть несколько “нехороших” мест.
Программное обеспечение одновременно искусство и ремесло. Тут однозначно требуется теория и практика в ее совершенствовании. В реальности, невозможно отделить искусство от ремесла.
Я верю в красоту кода. Хотя это относительно, так как то, что хорошо для новичка, часто лишено элегантности кода, написанного профессионалом. Знание доступных инструментов просто необходимо, чтобы освоить их. Например, только знание std::vector без алгоритмов из STL позволит написать работающий, но не элегантный код, даже если это оценивается только метрикой сложности McCabe’а (кстати, это еще один плагин для C++ CDT, разработанные нашим студентом).
Мы были первыми после Билла Опдайка, кто попытался реализовать рефакторинг для C++. Для этого мы переделали большие куски внутренностей CDT и реализовали генерацию кода на основе AST (abstract syntax tree). Так что только запуска тестов не достаточно – код еще должен быть “очищен”. Я много рецензирую код и вижу, как часто можно сделать лучше. Я правда не уверен, что талант можно автоматизировать, так как это смесь вкуса и опыта. Тут требуется не только видеть саму проблему, но и пути ее решения. С другой стороны, если посмотреть на примеры “дурно пахнущего” кода по Мартину Фаулеру, часто можно встретить противоречивые примеры.
Некоторые стандарты кодирование часто промоутируют то, что я называю “плохим” стилем, и привносят ненужные усложнения. Например, Гугл предлагает так называемые выходные параметры передавать через указатели, даже в С++. Это может делаться для того, чтобы на вызывающей стороне приходилось писать foo(&var), явно передавая переменную по указателю, тем самым улучшая читабельность. Однако, этот прием, необходимый в С, где не было ссылок, снова привносит проблемы, уже решенные введением ссылок в С++. В такой функции надо проверять указатель на nullptr (в терминах C++11), и никогда нельзя быть уверенным, что этот указатель правильный в случае ошибки в вызывающем коде, и надо игнорировать проблему, просто аварийно завершая программу, если был передан nullptr. Если решать проблему в стиле С++, то надо использовать ссылки, void foo(type & var);, и среда разработки покажет это, когда наведете мышкой на параметр, без явного использования амперсанда. Стандарт кодирования должен помогать использовать возможности языка и среды разработки для создания более компактного и простого кода, а не рассчитывать на старые редакторы, или просто на незнание их возможностей.
Стандарт кодирования или правила хороши только когда они выполняются. Средства, какие как (PC-)lint могут в этом помочь. Часто подобные классические утилиты работают из командной строки и применяются на стадии сборки. Более современные их варианты встроены в среду и делают не только анализ, но и предлагают на месте исправить проблему, например, FingBugs для Java или FXCop для C#. Их настройка, чтобы исключить ложные срабатывания, всегда непростая задача. Чтобы упростить применение lint мы создали Linticator, плагин для Eclipse, который автоматически настраивает lint и визуализируют результаты его работы в IDE (http://linticator.com).
Быть слишком самоуверенным, вместо написания (unit) тестов и регулярного их запуска. Любая разработка без автоматизированного тестирования является, на мой взгляд, непрофессиональным подходом.
Отсутствие архитектуры или знаний о ней является одной из больших проблем, которые я часто наблюдаю при рецензировании кода.
Ну и простая вещь: глобальные переменные под соусом синглтонов. Передавайте все зависимости извне как аргументы. Это также позволит понять, где зависимостей слишком много. Использование шаблонов, решающих эту проблему, и написание unit-тестов поможет избежать проблемы в целом на ранних стадиях.
Вариантов очень много. Я не хотел бы рекомендовать что-то конкретное, так как в каждом есть свои недостатки. Важнее изучить несколько языков, причем которые основаны на разных парадигмах: функциональные, объектно-ориентированные, динамические или со статическом типизацией, компилируемые, интерпретируемые, использующие виртуальную машину или компилируемые в машинный код. Говорят, что на любом языке можно написать программу на Фортране. Этого можно и не делать со множеством языков, но важно знать и применять языковые идиомы.
Да. Работа со студентами очень важна. И не только для их пользы, но и для моей, как преподавателя. Это позволяет улучшать программу обучения. Непосредственное взаимодействие очень важно для обеих сторон. Заочное обучение не дает этого, но является удобным, для тех, кто по каким-то причинам не может присутствовать лично. Но мне кажется, что сделать качественное заочное обучение весьма дорого.
Я слышал, люди используют аналогии из боевых искусств, например, упражнения по программированию (coding dojos). Я не владею боевыми искусствами, и мой единственный спорт, за исключением кардио-тренировок – это горные лыжи. Это не командный спорт, но позволяющий “поймать поток” и забыть обо всем. С работой тоже самое. Но программное обеспечение – это командный спорт, и отработка стандартных движений, вроде “ката”, помогает развивать подсознательный вкус к хорошему коду и стилю, но только если он у вас уже есть, или кто-то помогает вам его развить. По мой взгляд, есть проблема – многие преподаватели, обучающие новичков, не имеют практического опыта программирования (это относится и ко мне, но я по крайней мере стараюсь регулярно работать с кодом).
Я всегда прошу примеры кода от человека. Однажды, я завернул хорошую кандидатскую по той простой причине, что код был плохо написан.
В наши дни также важно уметь адаптироваться к изменениям и быть подкованным с точки зрения архитектуры. Кандидат должен понимать шаблоны программирования и знать, что существует больше, чем 23 GOF (Gang of Four Design Patterns) шаблона.
Я начал с выбора курса в университете. Однако, чем хорошо программное обеспечение, что мы можем создавать свои собственные инструменты. Это как бы если пекарь мог бы испечь себе новую печь или миксер для теста. Так что поучившись немного, я выбрал профессию. Я занимаюсь этим уже 30 лет, из них 25 лет преподаю (я начал это, когда был еще студентом), пишу об этом книги и статьи уже около 18 лет и все еще обожаю свою профессию. Я хочу сделать этот мир лучше для программистов и надеюсь это сделать, убрав плохие программы. Знаю, это, конечно, невозможно, но это дает мне понять, куда двигаться.
■
Спасибо Питер. С нетерпением ждем ваши новые проекты и выступления.
// Май 16, 2012
// Питер Соммерлад, Александр Дёмин
]]>#include <string> #include <cstdio> int main() { std::string s = "12345678"; std::printf("[%s]\n", s); }
Явная опечатка с пропущеным вызовом s.c_str()
. Но странно, что Студия, даже с /Wall
не дает никакого предупрежнения, и более того – код не падает. Но вот gcc
предупреждает:
warning: cannot pass objects of non-POD type 'struct std::string' through '...'; call will abort at runtime
и программа при запуске благополучно падает с Illegal instruction
.
Неужели в Студии специально сделали, что работал такое глюк просто потому, что это слишком распространенная опечатка?
]]>Итак, сразу же пузомерка с официального сайта.
Мне все-таки хотелось понять, что у них тут есть такого, ради чего они замутили этот проект. Я посмотрел их презентацию с OSCON 2011:
#1
#2
#3
Вот мои субъективные заметки:
А вот пара принципиальных решений:
Лично для меня эти два пункта являются ключевыми, и они не вписываются в мой стиль использования DVCS (именно по этой причине я отказался от fossil для своих проектов, хоть мне и очень нравилось иметь локальные Wiki и баг-трекер).
В итоге получается, ключевыми возможностями остаются “не-GPL” и внешние хранилища. То есть явная атака на корпоративное использование. Кстати, у основателя компании (Eric Sink) уже была компания и продукт по контролю версий, которые был куплены Майкрософтом.
Итак, мой субъективный вывод: это попытка привнести DVCS в мир корпораций.
Eric Sink написал книгу “Version Control by Example”, в которой есть более менее честное сравнение Veracity с основными VCS. Я пролистал ее за полчасика. Наткнулся на мега-цитату, суть которой яростно исповедую и активно продвигаю вокруг (выделение жирным мое).
]]>11. Don’t comment out code
When using a VCS, you shouldn’t comment out a big section of code simply because you think you might need it someday. Just delete it. The previous version of the file is still in your version control history, so you can always get it back if and when you need it. This practice is particularly important for web developers, where the commented-out stuff may adversely affect your page load times.
Никогда не использовать префикс get
для функций-getter’ов.
Обычно геттерами называют микро-функции или методы, которые элементарно возвращают ссылку или указатель на член класса, просто ради идеи не давать прямой доступ к нему. В них обычно нет какой логики.
Получается, эта функция, “геттер”, ничего не делает, что может описываться глаголом get
. Геттер – это просто синоним члена класса. Смысловая нагрузка слова get
тут начисто отсутствует. Префикс get
нужен только если под ним скрывается реальное действие или вычисление, например, getLastTick()
или getFullUserName()
. А еще лучше заменить слово get
на что-нибудь более информативное: extractLastTick()
или buildFullUserName()
.
■
Посты по теме:
]]>Получилось это случайно. Когда мы снимали жилье, на кухне не было место, что пристроить компьютер (да, не удивляйтесь, нам с женой на кухне нужен компьютер). И Ольга как-то нашла небольшой, но высокий стол и к нему барный стул в Икее. Кстати, когда дети были поменьше, такой стол еще и служил примитивной защитой от детей.
И мы начали использовать эту парочку как хотспот для “погуглить”, еду заказать и т.д. И как-то со временем мне эта идея нравилась все больше и больше. Работая таким образом, ты не зависаешь. Конечно, когда “идет процесс” и клавиатура дымится, то можно пристроить зад на барный стул. После переезда, на кухне уже полно места, и я забрал эти стол и стул к себе в “кабинет”. Вот как это выглядит:
Кстати, к ножкам стола прекрасно крепятся хомутами всякие удлинители, провода и т.д. Так я работаю уже более двух лет.
А вот для сравнения старые стол и стул.
P.S. Традиционная рубрика “ну чтобы два раза не вставать”. Я сугубо убежден, что человек, придумавший компьютерные столы с выдвижной подставкой для клавиатуры, никогда не работал на клавиатуре. Сложно представить неудобнее позиции для рук. И еще, лично я не могу использовать офисные стулья, на которых неудобно сидеть, подсунув под себя ногу.
]]>Leonard Richardson, Sam Ruby, “RESTful Web Services”
Итак, если опустить нудное описание HTTP библиотек Ruby, Python, Java и curl, еще более нудное описание форматов HTML4, XHTML, HTML5, Atom, XML, чуть менее нудное описание нескольких Ajax-библиотек, крайне нудное описание стандартных response-кодов HTTP, нереально нудное описание стандартных заголовков HTTP, то суть книги можно выразить весьма кратко. Вот REST в моем превратном, но кратком изложении.
При проектировании web-сервиса следует:
http://domain/engine.php?func=123&id=test
для получения данных о пользователе, должно быть http://domain/users/test
.Все! Вы знаете содержимое этой книги.
Редкий случай, когда ну очень хочется попросить деньги назад за книгу. Продать ее не получится, ибо электронная она.
]]>Вчера как-то был настрой, и решил я поставить туда RetroBSD.
RetroBSD – это настоящий UNIX, являющийся наследником 2.11BSD и предназначенный для встраиваемых систем с фиксированной структурой памяти. На текущий момент работает на микроконтроллере Microchip PIC32 с 128 килобайтами памяти и 512 килобайтами Flash. Данный процессор позволяет разделять адресное пространство ядра и пользовательских процессов.
RetroBSD обеспечивает защиту памяти ядра, используя аппаратные возможности микропроцессора, полноценную вытесняющую многозадачность, POSIX API (fork, exec, wait4 и т.д.). Можно писать программы прямо на устройстве, так как есть компилятор С. Ядро системы прошивается в кристалл, а файловая система загружается с SD-карты.
RetroBSD поддерживает не только аппаратуру Maximite, но и ряд альтернативных устройств на базе PIC32 (chipKIT Max32, Sparkfun UBW32, Microchip Explorer 16, Microchip PIC32 USB/Ethernet Starter Kit, Olimex Duinomite, Duinomite-Mini и Duinomite-Mega, eflightworks).
После небольшой возни с программой-загрузчиком и установкой необходимых библиотек, получилось все собрать и залить на устройство.
Торжественный момент включения, и…
Это UNIX!
Сначала, конечно, игры. Питон, он же Червяк.
Пасьянс:
Я теперь немного посерьезнее – Forth.
На данный момент RetroBSD работает только через последовательный порт и не поддерживает VGA и PS/2, но у Сергея Вакуленко, автора RetroBSD, есть планы написать эти драйвера.
]]>Но! Если в Хроме открыть Developer Tools, и, поелозив по странице, немного подкрутить CSS, в основном добавив display: none
там-сям, то получается вот такая вот красота:
Вопрос: кто-нибудь может указать на какой-нибудь API в Хроме или готовое расширение, которым можно было автоматически править CSS? Я видел подобные расширения для Хрома, которыми можно менять и убирать элементы Gmail и Google Reader, но они были заточены именно под продукты Гугла. Универсального я никак не могу найти.
Решение найдено. Надо поставить Stylebot и добавить в нем CSS-фильтр на адрес www.translate.ru. Текст CSS-добавки ниже. После этого страничка будет содержать только минимальный набор нужных элементов.
#contentBlock header { display: none; } #ctl00_SiteContent_templatesBlock { display: none; } #passportMenu { display: none; } #bott_link { display: none; } topAdvert { display: none; } #topAdvert { display: none; } #subscribeForm { display: none; } #adv4Blocks { display: none; } #bottomAdvSection { display: none; } #blogRSS { display: none; } #rightSection { display: none; } #viewMode { display: none; } #viewModeBlock { display: none; } #tmOther { display: none; } #resultBlock { float: none; width: 100%; } #sourceBlock { float: none; width: 100%; } #mainSection { margin: 0px; } #contentBlock { border: none; border-radius: 0px; box-shadow: 0 0; margin: 0; max-width: 100%; } #dir_set { margin: 0; } #dmenu { float: none; } #btr_web { float: none; } .selDiv { padding: 0; float: none; } #ctl00_SiteContent_rLang { min-width: 0; } #ctl00_SiteContent_sLang { min-width: 0; } select { border: 1px solid; } .wrapp { padding: 0; } #bTranslate { margin-left: 1em; padding: 2px; font-weight: normal; background-color: #ccc; -webkit-border-radius: 2px; color: #003333; } body { background: none; }]]>
Для начала вариант вообще без какой-либо синхронизации:
#include <future> #include <iostream> volatile int value = 0; int loop(bool inc, int limit) { std::cout << "Started " << inc << " " << limit << std::endl; for (int i = 0; i < limit; ++i) { if (inc) { ++value; } else { --value; } } return 0; } int main() { auto f = std::async(std::launch::async, std::bind(loop, true, 20000000)); loop(false, 10000000); f.wait(); std::cout << value << std::endl; }
Компилировать будем clang’ом:
clang++ -std=c++11 -stdlib=libc++ -O3 -o test test.cpp && time ./test
Запускаем:
SSttaarrtteedd 10 2100000000000000
11177087
real 0m0.070s
user 0m0.089s
sys 0m0.002s
Видно, что операции операторов инкремента и декремента неатомарные, и переменная value
в конце содержит мусор.
#include <future> #include <iostream> volatile int value = 0; int loop(bool inc, int limit) { std::cout << "Started " << inc << " " << limit << std::endl; for (int i = 0; i < limit; ++i) { if (inc) { asm("LOCK"); ++value; } else { asm("LOCK"); --value; } } return 0; } int main() { auto f = std::async(std::launch::async, std::bind(loop, true, 20000000)); loop(false, 10000000); f.wait(); std::cout << value << std::endl; }
Запускаем:
SSttaarrtteedd 10 2000000100000000
10000000
real 0m0.481s
user 0m0.779s
sys 0m0.005s
Тут уже value
содержит правильное значение, но, конечно, это абсолютно непереносимый хак, заточенный под x86, и который, например, у меня работает только с -O3
(с -O2
падает с “illegal instruction”, так как между командой LOCK и инкрементом/декрементом компилятор сует что-то еще).
#include <future> #include <iostream> #include "boost/interprocess/detail/atomic.hpp" using namespace boost::interprocess::ipcdetail; volatile boost::uint32_t value = 0; int loop(bool inc, int limit) { std::cout << "Started " << inc << " " << limit << std::endl; for (int i = 0; i < limit; ++i) { if (inc) { atomic_inc32(&value); } else { atomic_dec32(&value); } } return 0; } int main() { auto f = std::async(std::launch::async, std::bind(loop, true, 20000000)); loop(false, 10000000); f.wait(); std::cout << atomic_read32(&value) << std::endl; }
Запускаем:
SSttaarrtteedd 10 2100000000000000
10000000
real 0m0.457s
user 0m0.734s
sys 0m0.004s
Результат правильный, и время почти такое же, как и с командой LOCK
. Неудивительно, так как atomic
на самом тоже использует инструкцию LOCK
, но просто точно гарантированным и проверенным образом.
#include <future> #include <iostream> #include "boost/smart_ptr/detail/spinlock.hpp" boost::detail::spinlock lock; volatile int value = 0; int loop(bool inc, int limit) { std::cout << "Started " << inc << " " << limit << std::endl; for (int i = 0; i < limit; ++i) { std::lock_guard<boost::detail::spinlock> guard(lock); if (inc) { ++value; } else { --value; } } return 0; } int main() { auto f = std::async(std::launch::async, std::bind(loop, true, 20000000)); loop(false, 10000000); f.wait(); std::cout << value << std::endl; }
Запускаем:
SSttaarrtteedd 10 2100000000000000
10000000
real 0m0.541s
user 0m0.675s
sys 0m0.089s
Немного медленнее, но не на много.
#include <future> #include <iostream> std::mutex mutex; volatile int value = 0; int loop(bool inc, int limit) { std::cout << "Started " << inc << " " << limit << std::endl; for (int i = 0; i < limit; ++i) { std::lock_guard<std::mutex> guard(mutex); if (inc) { ++value; } else { --value; } } return 0; } int main() { auto f = std::async(std::launch::async, std::bind(loop, true, 20000000)); loop(false, 10000000); f.wait(); std::cout << value << std::endl; }
Запускаем:
SSttaarrtteedd 10 2010000000000000
10000000
real 0m25.229s
user 0m7.011s
sys 0m22.667s
Вот тут, конечно, реально медленнее.
Метод | Время (сек.) |
Без синхронизации | 0.070 |
LOCK | 0.481 |
Atomic | 0.457 |
Spinlock | 0.541 |
Mutex | 22.667 |
Конечно, очень многое зависит от платформы и компилятора (я тестировал на Mac Air и clang). Но лично я, например, получил интересное наблюдение, что spinlock, несмотря на значительную сложную реализации, судя по сгенерированному кому, почти не уступает atomic’у.
Жалко, что мой clang пока не поддерживает atomic
, и пришлось использовать boost.
В C++ 2011 гарантируется, что при многопоточном использовании стандартных потоков гарантируется отсутствие data race, но неперемешивание вывода не гарантируется.
Concurrent access to a synchronized (§27.5.3.4) standard iostream object’s formatted and unformatted input (§27.7.2.1) and output (§27.7.3.1) functions or a standard C stream by multiple threads shall not result in a data race (§1.10). [ Note: Users must still synchronize concurrent use of these objects and streams by multiple threads if they wish to avoid interleaved characters. — end note ]
T*
или T&
(#2). Поэтому, когда мне надо было реализовать Pimpl, я не использовал умные указатели, так как с виду для них требуется полное определение класса.
A.h
:
#include <memory> class A_pimpl; class A { … std::unique_ptr<A_pimpl> p; }
Я почему-то думал, что это не будет работать из-за неопределенности класса A_pimpl
. И был сильно удивлен, попробовал и узнав, что на самом деле это прекрасно работает. То есть факт #1 не эквивалентен факту #2.
В A.cpp
можно теперь спокойно писать:
#include "A.h" #include "pimpl.h" A::A() : p(new A_pimpl()) {}
Все выше сказанное также работает для std::shared_ptr
(C++ 2011), boost::scoped_ptr
и boost::shared_ptr
.
Дополнение
Как меня поправили в комментариях, у класса A
обязательно должен быть явно задан деструктор, причем его тело должно быть именно в A.cpp
, а не в заголовочном файле. Иначе будет ошибка типа “error C2338: can’t delete an incomplete type”.
A.h
:
#include <memory> class A_pimpl; class A { A(); ~A(); std::unique_ptr<A_pimpl> p; }
и A.cpp
:
#include "A.h" #include "pimpl.h" A::A() : p(new A_pimpl()) {} A::~A() {}]]>
Его презентация доступна с примерами.
Вторая его презентация, увы, которую я не посетил, называлась “Dataflow, Actors and High Level Structures in Concurrent Applications”. Фактически, там было показано, как можно на С++ забацать actor’ы практически как в Эрланге.
Эта презентация тоже доступна с примерами.
Рекомендую.
Идем далее. Недавно вышла книга, “C++ Concurrency in Action” этого же автора (Anthony Williams).
Если хотите знать все про memory model в C++, почитайте пятую главу (и не только пятую).
]]>Я читал его много раз, но так и не мог понять – почему именно так?
В итоге, я нашел вот эту страницу – Dividing The Plane, где есть такое предложение:
Let’s say that we’ve got n lines (for some arbitrary n). And we add an n+1th line. That line goes through region-line-region-line-…-line-region. It went through n lines and n+1 regions (assuming that all of the lines intersect). For each region that it went through, it added a region (split that region into two regions).
И вот эта фраза на пальцах объясняет, почему именно n
-я прямая добавляет n
регионов. Идея тут в визуализации прохода новой прямой через существующие регионы.
#include <future> int main(int argc, char* argv[]) { for (auto i = 0L; i < 1000000; ++i) { auto f = std::async([](){ return 0; }); f.get(); } return 0; }
Данный код стабильно падает. Исключений не бросает. Интересно, что если уменьшать количество итерации, то падения изчезают.
Компилятор cl.exe 17.00.40825.2, студия 11.0.40825.2 PREREL. Запостил на Stack Overflow. Пока говорят, что скорее всего реально баг.
Где обычно файлят баги в VS?
]]>Вот мой наивный велосипед:
int naive_quick_sort(std::vector<Type>::iterator begin, std::vector<Type>::iterator end) { auto const sz = end - begin; if (sz <= 1) return 0; auto pivot = begin + sz/2; auto const pivot_v = *pivot; std::swap(*pivot, *(end - 1)); auto p = std::partition(begin, end, [&](const Type& a) { return a < pivot_v; } ); std::swap(*p, *(end - 1)); if (sz > 4096) { auto left = std::async(std::launch::async, [&]() { return naive_quick_sort(begin, p); }); naive_quick_sort(p + 1, end); } else { naive_quick_sort(begin, p); naive_quick_sort(p + 1, end); } return 0; } void quick_sort(std::vector<Type>& arr) { naive_quick_sort(arr.begin(), arr.end()); }
Реализация крайне простая, но стоит отметить несколько моментов. Есть некая константа 4096
, которая определяет порог, когда отключается параллельное выполнение. Почему именно такое значение? Не знаю. Взято из воздуха с минимальным чувством здравого смысла. Когда же параллельность активна, то сортировка левого массива запускается через async
в другом потоке, а правый сортируется как и раньше в текущем потоке. При выходе из контекста функции гарантируется, что задача, запущенная через async
, будет завершена (ее завершения будут ждать).
Традиционно, пузомерка. Три кандидата:
async
)if (sz > 4096)
заменить на if (false)
)naive_quick_sort(arr.begin(), arr.end())
заменить на std::sort(arr.begin(), arr.end())
)Сортируется массив из 50000000 элементов типа int64
(со знаком). Делается 10 экспериментов, и считается среднее. Значения генерируются случайно:
std::tr1::uniform_int<Type> uniform( std::numeric_limits<Type>::min(), std::numeric_limits<Type>::max()); std::mt19937_64 engine; void generate(std::vector<Type>& v) { std::for_each(v.begin(), v.end(), [](Type& i) { i = uniform(engine); }); }
Не спрашивайте, почему тут делается перегон из big endian туда и обратно. Это было сделано для сравнения с другой программой, на Java. При замерах времени учитывается только “чистое время”.
Компилятор VS 2011, 64-bit. Процессор Intel Core i5 2.53GHz, 4 ядра.
Итерация Через async() Один поток std::sort()
--------- --------------- ------------ ------------
1 2512 6555 7309
2 2337 6320 6977
3 2450 6516 7180
4 2372 6388 6933
5 2387 7074 7189
6 2339 7399 7040
7 2434 6875 7040
8 2562 7060 7187
9 2470 7050 7145
10 2422 6846 6898
--------- --------------- ------------ ------------
Среднее 2428.5 6808.3 7089.8
Время указано в миллисекундах.
Получается где-то в три раза быстрее. Странное небольшое отставание std::sort()
скорее всего связано с тем, что данные “хорошие”, и на них моей простецкой реализации просто везет. Видно, что у времени std::sort()
девиация гораздо меньше. Все-таки stl::sort()
стабилен по времени вне зависимости от данных.
Есть ли в этой параллельности практическая польза? Думаю нет. Очень сложно оценить стабильность алгоритма на разных данных. Например, совершенно не ясно, как выбрать порог отключения многозадачности? Стоит ли использовать пул потоков?
Если кому интересно, внизу полный текст этого велосипеда, включая генератор данных.
Сборка и генерация данных:
call "%VS110COMNTOOLS%..\..\VC\vcvarsall.bat" amd64 && ^
cl /Ox /DWIN32 sort_async.cpp && ^
sort_async generate
Осторожно! Генератор создаст данных на 8 гигов.
Сборка и эксперимент:
call "%VS110COMNTOOLS%..\..\VC\vcvarsall.bat" amd64 && ^
cl /Ox /DWIN32 sort_async.cpp && ^
sort_async
Файл sort_async.cpp
:
#include <vector> #include <iostream> #include <fstream> #include <sstream> #include <algorithm> #include <iomanip> #include <future> #include <random> #include <chrono> #include <cstdlib> const int ITERATIONS_NUM = 10; const int DATA_SIZE = 50000000; typedef __int64 Type; inline void endian_swap(Type& x) { x = (0x00000000000000FF & (x >> 56)) | (0x000000000000FF00 & (x >> 40)) | (0x0000000000FF0000 & (x >> 24)) | (0x00000000FF000000 & (x >> 8)) | (0x000000FF00000000 & (x << 8)) | (0x0000FF0000000000 & (x << 24)) | (0x00FF000000000000 & (x << 40)) | (0xFF00000000000000 & (x << 56)); } std::tr1::uniform_int<Type> uniform( std::numeric_limits<Type>::min(), std::numeric_limits<Type>::max()); std::mt19937_64 engine; void generate(std::vector<Type>& v) { std::for_each(v.begin(), v.end(), [](Type& i) { i = uniform(engine); }); } void check_sorted(const std::vector<Type>& v, const std::string& msg) { for (auto i = 0; i < v.size() - 1; ++i) { if (v[i] > v[i + 1]) { std::cout << "\nUnsorted: " << msg << "\n"; std::cout << "\n" << i << "\n"; std::cout << v[i] << " " << v[i + 1] << "\n"; std::exit(1); } } } std::string data_file_name(const int i, const std::string& suffix) { std::ostringstream fmt; fmt << "trash_for_sort_" << i << suffix << ".bin"; return fmt.str(); } void save_file(std::vector<Type> array, const std::string& name) { std::for_each(array.begin(), array.end(), [](Type& i) { endian_swap(i); }); std::ofstream os(name.c_str(), std::ios::binary|std::ios::out); auto const bytes_to_write = array.size() * sizeof(array[0]); std::cout << "Saving " << array.size() << " bytes to " << name << "\n"; os.write((char *)&array[0], bytes_to_write); } int main_generate(int argc, char* argv[]) { std::cout << "Generation\n"; for (auto i = 0; i < ITERATIONS_NUM; ++i) { std::vector<Type> unsorted(DATA_SIZE); generate(unsorted); save_file(unsorted, data_file_name(i, "")); std::cout << "Sorting...\n"; std::sort(unsorted.begin(), unsorted.end()); check_sorted(unsorted, "check sorted array"); save_file(unsorted, data_file_name(i, "_sorted")); } return 0; } void load_file(std::vector<Type>& array, const std::string& name) { std::cout << "Loading " << name; array.resize(DATA_SIZE, 0); std::ifstream is(name.c_str(), std::ios::binary|std::ios::in); auto const to_load = array.size() * sizeof(array[0]); is.read((char *)&array[0], to_load); if (is.gcount() != to_load) { std::cerr << ", Bad file " << name << ", loaded " << is.gcount() << " words but should be " << to_load << "\n"; std::exit(1); } std::for_each(array.begin(), array.end(), [](Type& v){ endian_swap(v); }); } int naive_quick_sort(std::vector<Type>::iterator begin, std::vector<Type>::iterator end) { auto const sz = end - begin; if (sz <= 1) return 0; auto pivot = begin + sz/2; auto const pivot_v = *pivot; std::swap(*pivot, *(end - 1)); auto p = std::partition(begin, end, [&](const Type& a) { return a < pivot_v; } ); std::swap(*p, *(end - 1)); if (sz > 4096) { auto left = std::async(std::launch::async, [&]() { return naive_quick_sort(begin, p); }); naive_quick_sort(p + 1, end); } else { naive_quick_sort(begin, p); naive_quick_sort(p + 1, end); } return 0; } void quick_sort(std::vector<Type>& arr) { naive_quick_sort(arr.begin(), arr.end()); } int main(int argc, char* argv[]) { if (argc == 2 && !std::strcmp(argv[1], "generate")) return main_generate(argc, argv); std::vector<double> times; auto times_sum = 0.0; for (auto i = 0; i < ITERATIONS_NUM; ++i) { std::vector<Type> unsorted; load_file(unsorted, data_file_name(i, "")); std::vector<Type> verify; std::cout << ", "; load_file(verify, data_file_name(i, "_sorted")); check_sorted(verify, "verify array"); std::cout << ", Started"; auto start = std::chrono::high_resolution_clock::now(); quick_sort(unsorted); auto stop = std::chrono::high_resolution_clock::now(); std::cout << ", Stopped, "; auto duration = std::chrono::duration<double>(stop - start).count(); std::cout << duration; check_sorted(unsorted, "sorted array"); const auto match = unsorted == verify; std::cout << (match ? ", OK" : ", DON'T MATCH"); times.push_back(duration); times_sum += duration; std::cout << "\n"; } auto const average = times_sum / ITERATIONS_NUM; auto const max_element = *std::max_element(times.begin(), times.end()); auto const min_element = *std::min_element(times.begin(), times.end()); auto const average_fixed = (times_sum - max_element - min_element) / (ITERATIONS_NUM - 2); std::cout << "Average: " << average << "s, " << "Average without max/min: " << average_fixed << "s." << std::endl; }
Под занавес, картинка загрузки процессоров. Явно видны всплески на каждой итерации, когда система используется подзавязку.
В комментариях есть интересная ссылка на статью “Dynamic Task Parallelism” от Microsort, где также приводится вариант многопоточного QuickSort’a.
]]>YYYY
, MM
, mm:ss
и т.д. Но вот в процессе написания движка для блога, я наткнулся реализацию в Go. В Go в качестве шаблона используются не особые символы, а фиксированные значения непосредственно даты или времени. Например:
func format_time(t time.Time) string { return t.Format("2006.01.02-15.04.05") // Аналогично: YYYY.MM.DD-hh.mm.ss }
Вот полный набор “волшебных” значений (из time/format.go):
stdLongMonth = "January" stdMonth = "Jan" stdNumMonth = "1" stdZeroMonth = "01" stdLongWeekDay = "Monday" stdWeekDay = "Mon" stdDay = "2" stdUnderDay = "_2" stdZeroDay = "02" stdHour = "15" stdHour12 = "3" stdZeroHour12 = "03" stdMinute = "4" stdZeroMinute = "04" stdSecond = "5" stdZeroSecond = "05" stdLongYear = "2006" stdYear = "06" stdPM = "PM" stdpm = "pm" stdTZ = "MST" stdISO8601TZ = "Z0700" // prints Z for UTC stdISO8601ColonTZ = "Z07:00" // prints Z for UTC stdNumTZ = "-0700" // always numeric stdNumShortTZ = "-07" // always numeric stdNumColonTZ = "-07:00" // always numeric
И некоторые примеры готовых шаблонов:
ANSIC = "Mon Jan _2 15:04:05 2006" UnixDate = "Mon Jan _2 15:04:05 MST 2006" RubyDate = "Mon Jan 02 15:04:05 -0700 2006" RFC822 = "02 Jan 06 15:04 MST" RFC822Z = "02 Jan 06 15:04 -0700" // RFC822 with numeric zone RFC850 = "Monday, 02-Jan-06 15:04:05 MST" RFC1123 = "Mon, 02 Jan 2006 15:04:05 MST" RFC1123Z = "Mon, 02 Jan 2006 15:04:05 -0700" // RFC1123 with numeric zone RFC3339 = "2006-01-02T15:04:05Z07:00" RFC3339Nano = "2006-01-02T15:04:05.999999999Z07:00" Kitchen = "3:04PM" // Handy time stamps. Stamp = "Jan _2 15:04:05" StampMilli = "Jan _2 15:04:05.000" StampMicro = "Jan _2 15:04:05.000000" StampNano = "Jan _2 15:04:05.000000000"
Я такого приема раньше не встречал, и как-то кажется сейчас, что это более наглядно.
]]>std::endl
– это всегда правильнее, чем \n
. Переносимость, и все такое. Но, увы.
Код с std::endl
:
#include <string> #include <iostream> int main() { for (int i = 0; i < 1000000; ++i) { std::string s(1, 'x'); std::cout << s << std::endl; } return 0; }
Компилируем и запускаем:
clang++ -o endl -O3 endl.cpp && time ./endl >rubbish
real 0m4.518s
user 0m1.080s
sys 0m3.311s
Код с \n
:
#include <string> #include <iostream> int main() { for (int i = 0; i < 1000000; ++i) { std::string s(1, 'x'); std::cout << s << '\n'; } return 0; }
Компилируем и запускаем:
clang++ -o endl -O3 endl.cpp && time ./endl >rubbish
real 0m0.263s
user 0m0.236s
sys 0m0.008s
Разница очевидна.
std::endl
всегда flush
ит поток, сбрасывая буфера. \n
же просто пихает в поток символ начала новой строки, и большинстве случаев это и требуется. А когда надо, можно отдельно вызвать std::flush
, один раз.
Вчера приехала эта книга. Хотел я что-то конкретное написать, но передумал, ибо в этом нет смысла. Книгу надо просто иметь, если есть интерес в новом стандарте C++ сегодня. Тут приводится исчерпывающее описание новой версии STL, естественно, с описанием новых возможностей самого языка. Много примеров. Как мне кажется, это пока первая книга по C++ 2011 такого калибра.
Лично мне пока даже просто интересно листать оглавление и глядеть, что и как вообще есть в новом языке и библиотеках (например, открыл для себя существование chrono).
Чтобы два раза не вставать. Обычно я пользуюсь сайтом http://www.cplusplus.com/ качестве справочника по C++. Для нового стандарта появился еще один – http://en.cppreference.com/w/.
]]>Такой подход называется live coding (типа, что пишу, то и сразу вижу).
Честно говоря, я думал, что очень многое из его демонстраций - это хардкод. Но оказывается, есть реальный язык, который можно пощупать, дающий схожие возможности. Зацените видеo:
Язык называется circa. Пока только в статусе alpha. По моему дилетантскому в разработке игр мнению, это очень удобно для отлаживания gameplay’я, хотя бы для 2D игр.
Интересная статья автора про подход по сохранению состояния runtime’а во время изменений кода “наживую”.
]]>assert
, который есть везде, является достаточным условием для использования тестов.
Например, есть мини-проект в один файл, и не охота тащить Google Test или cmockery. Я обычно делаю что-то такое:
#include <cassert> void foo(...) { // something } ... #ifdef UNIT_TESTING void Test_for_a_particular_use_case() { // Initialization ... assert(condition_1); ... assert(condition_N); } ... int main(...) { Test_for_a_particular_use_case(); std::cout << “All tests passed.” << std::endl; return 0; } #else int main(...) { // a proper main } #endif
Тут, конечно, не так удобно, так как иногда забываешь добавить запуск теста в main()
, и вроде как бы все работает, но на самом деле просто тест не запускается.
Но! Все это мелочи, по сравнению с удобством, которое дают тесты.
Кстати, заметил за собой интересую привычку. Практически всегда, когда пишу функцию, работающую с файлами, всегда рождаются две, например:
void FunctionDoingSomethingFromStream(std::istream* is) { ... } void FunctionDoingSomethingFromFile(const std::string& filename) { std::ifstream is(filename); return FunctionDoingSomethingFromStream(&is); }
Первая функция прекрасно тестируется, так как ей можно подсунуть std::istringstream
с тестовыми данными. Вторую же можно практически не тестировать (ну разве что на фазе QA, на реальных файлах, но не в unit-тестах в процессе сборки).
Еще интересный приемчик от Кевлина Хенни. У теста обычно есть объект тестирования (класс или функция), начальное условие (pre-condition) и конечное состояние (post-condition). И данные фазы удобно подчеркнуть в комментариях словами “Given”, “When” и “Then”, например:
void Test_for_a_particular_use_case_to_check() { // Given: ClassToTest a; // When: a.do_this(...); a.do_that(...); a.setup_something(...); // Then: assert(condition_1); ... assert(condition_N); }
Явное разделение этих фаз провоцирует правильную структуру теста: без циклов и условий, и работа только по одному сценарию (для другого сценария будет другой тест).
■
]]>Вот, например, моя переменная PS1
:
\W `vcprompt -f "%m%u %s:%b"`\$
И мое приглашение в bash
выглядит, например, так:
_engine +? git:master$
Я вижу, что я в каталоге _engine
, в каталоге существует репозиторий git, текущая ветка master
, есть измененные файлы (+
), и есть новые, еще не добавленные в git файлы (?
). Если в текущем каталоге нет никакого репозитория, приглашения будет как обычно.
vcprompt поддерживает не только git, а также bzr, cvs, darcs, fossil, hg, svn.
Написана на Питоне. Увы, для Windows, скорее все, прикрутить не получится.
]]>Вкратце, TCP/IP proxy - это программа, которая умеет принимать соединения и “пробрасывать” их на указанный адрес. Попутно ведутся логи переданных данных. Это очень удобно при отладке различных самодельных сетевых протоколов.
В плане функциональности версия на Go, как и эрланговская, ведет три лога: двунаправленный шестнадцатеричный дамп и бинарные логи в обоих направлениях, “от” и “к” удаленному хосту. Питоновская версия бинарные логи не ведет.
Конечно, все многопоточно. И так как в Go параллельное программирование настолько просто (и безопасно), количество параллельных активностей для каждого соединения даже больше, чем в версии на Эрланге.
На Эрланге для каждого соединения работали следующие четыре потока:
В версии на Go немного иначе:
Итого, 5.
В обоих случаях потоки чтения логируют данные, посылая сообщения потокам-логгерам. Конечно, нет никаких глупостей типа мьютексов или условных переменных. Проблемы согласования элегантно решаются через каналы Go.
Ниже привожу исходник. Он отличается от того, что в репозитории, наличием обильных комментариев. Для людей, не очень знакомых с Go, могут быть интересны некоторые моменты.
package main import ( "flag" "fmt" "net" "os" "strings" "time" "encoding/hex" "runtime" ) var ( host *string = flag.String("host", "", "target host or address") port *string = flag.String("port", "0", "target port") listen_port *string = flag.String("listen_port", "0", "listen port") ) func die(format string, v ...interface{}) { os.Stderr.WriteString(fmt.Sprintf(format+"\n", v...)) os.Exit(1) } // Данная функция реализует поток для двунаправленного дампа. func connection_logger(data chan []byte, conn_n int, local_info, remote_info string) { log_name := fmt.Sprintf("log-%s-%04d-%s-%s.log", format_time(time.Now()), conn_n, local_info, remote_info) logger_loop(data, log_name) } // Данная функция реализует двоичный лог. func binary_logger(data chan []byte, conn_n int, peer string) { log_name := fmt.Sprintf("log-binary-%s-%04d-%s.log", format_time(time.Now()), conn_n, peer) logger_loop(data, log_name) } // Данная функция реализует поток логирования. Создает лог-файл и начинает // принимает сообщения. Каждое сообщение - это кусок данных для помещения // в лог. Если пришли пустые данные - выходим. // func logger_loop(data chan []byte, log_name string) { f, err := os.Create(log_name) if err != nil { die("Unable to create file %s, %v\n", log_name, err) } defer f.Close() // Гарантируем закрытие файла в случае падения. for { b := <-data if len(b) == 0 { break } f.Write(b) f.Sync() // На всякий случай flush'имся. } } func format_time(t time.Time) string { return t.Format("2006.01.02-15.04.05") } func printable_addr(a net.Addr) string { return strings.Replace(a.String(), ":", "-", -1) } // Структура, в которой передаются параметры соединения. Объединено, чтобы // не таскать много параметров. type Channel struct { from, to net.Conn logger, binary_logger chan []byte ack chan bool } // Функция, "качающая" данные из одного сокета и передающая их в другой. // Попутно ведется логирование. func pass_through(c *Channel) { from_peer := printable_addr(c.from.LocalAddr()) to_peer := printable_addr(c.to.LocalAddr()) b := make([]byte, 10240) offset := 0 packet_n := 0 for { n, err := c.from.Read(b) if err != nil { c.logger <- []byte(fmt.Sprintf("Disconnected from %s\n", from_peer)) break } if n > 0 { // Если что-то пришло, то логируем и пересылаем на выход. c.logger <- []byte(fmt.Sprintf("Received (#%d, %08X) %d bytes from %s\n", packet_n, offset, n, from_peer)) // Это все, что нужно для преобразования в hex-дамп. Удобно, не так ли? c.logger <- []byte(hex.Dump(b[:n])) c.binary_logger <- b[:n] c.to.Write(b[:n]) c.logger <- []byte(fmt.Sprintf("Sent (#%d) to %s\n", packet_n, to_peer)) offset += n packet_n += 1 } } c.from.Close() c.to.Close() c.ack <- true // Посылаем сообщение в главный поток, что мы закончили. } // Данная функция обслуживает соединение. Запускает необходимые потоки и ждет // их завершения. func process_connection(local net.Conn, conn_n int, target string) { // Соединяемся к удаленном сокету, куда будем пересылать данные. remote, err := net.Dial("tcp", target) if err != nil { fmt.Printf("Unable to connect to %s, %v\n", target, err) } local_info := printable_addr(remote.LocalAddr()) remote_info := printable_addr(remote.RemoteAddr()) // Засекаем начальное время. started := time.Now() // Создаем каналы для обмена с логгерами. logger := make(chan []byte) from_logger := make(chan []byte) to_logger := make(chan []byte) // Канал для получения подтверждений от качающих потоков. ack := make(chan bool) // Запускаем логгеры. go connection_logger(logger, conn_n, local_info, remote_info) go binary_logger(from_logger, conn_n, local_info) go binary_logger(to_logger, conn_n, remote_info) logger <- []byte(fmt.Sprintf("Connected to %s at %s\n", target, format_time(started))) // Запускаем качающие потоки. go pass_through(&Channel{remote, local, logger, to_logger, ack}) go pass_through(&Channel{local, remote, logger, from_logger, ack}) // Ждем подтверждения об их завершении. <-ack <-ack // Вычисляем длительность соединения. finished := time.Now() duration := finished.Sub(started) logger <- []byte(fmt.Sprintf("Finished at %s, duration %s\n", format_time(started), duration.String())) // Посылаем логгерам команды закругляться. Мы тут не ждем от них // подтверждения, так как они и так завершатся рано или поздно, а они нам // более не нужны. logger <- []byte{} from_logger <- []byte{} to_logger <- []byte{} } func main() { // Просим Go использовать все имеющиеся в системе процессоры. runtime.GOMAXPROCS(runtime.NumCPU()) // Разбираем командную строку (несложно, не правда ли?) flag.Parse() if flag.NFlag() != 3 { fmt.Printf("usage: gotcpspy -host target_host -port target_port -listen_post=local_port\n") flag.PrintDefaults() os.Exit(1) } target := net.JoinHostPort(*host, *port) fmt.Printf("Start listening on port %s and forwarding data to %s\n", *listen_port, target) ln, err := net.Listen("tcp", ":"+*listen_port) if err != nil { fmt.Printf("Unable to start listener, %v\n", err) os.Exit(1) } conn_n := 1 for { // Ждем новых соединений. if conn, err := ln.Accept(); err == nil { // Запускаем поток обработки соединения. go process_connection(conn, conn_n, target) conn_n += 1 } else { fmt.Printf("Accept failed, %v\n", err) } } }
Повторюсь, каждое соединения обслуживается пятью потоками. И сделал я это не ради прикола. Просто мне показалось, что логически есть явно независимые подзадачи, которые было бы логично запустить параллельно. Если б я писал все на C++/boost, я б скорее всего замутил все одном потоке для каждого соединения (а может быть и вся программа была бы чисто однопотоковой через какие-нибудь изощренные библиотеки мультиплексирования), и не исключено, что на C++ в итоге еще и работало бы быстрее, несмотря на один поток. Но я хочу сказать не об этом. Go подталкивает на многопоточное программирование (а не отталкивает, как C++, даже на стероидах нового стандарта). Так или иначе, будут задачи, где удобная многопоточность станет ключевым фактором.
Запустить можно так (требуется как минимум Go релиз 1):
go run gotcpspy.go -host pop.yandex.ru -port 110 -local_port 8080
Выведется:
Start listening on port 8080 and forwarding data to pop.yandex.ru:110
Затем, если в другом окне ввести:
telnet localhost 8080
и ввести, например, USER test
<ENTER>
и PASS none
<ENTER>
, то будут созданы три лога (дата в имени будет, конечно, другая).
Общий лог log-2012.04.20-19.55.17-0001-192.168.1.41-49544-213.180.204.37-110.log
:
Connected to pop.yandex.ru:110 at 2012.04.20-19.55.17
Received (#0, 00000000) 38 bytes from 192.168.1.41-49544
00000000 2b 4f 4b 20 50 4f 50 20 59 61 21 20 76 31 2e 30 |+OK POP Ya! v1.0|
00000010 2e 30 6e 61 40 32 36 20 48 74 6a 4a 69 74 63 50 |.0na@26 HtjJitcP|
00000020 52 75 51 31 0d 0a |RuQ1..|
Sent (#0) to [--1]-8080
Received (#0, 00000000) 11 bytes from [--1]-8080
00000000 55 53 45 52 20 74 65 73 74 0d 0a |USER test..|
Sent (#0) to 192.168.1.41-49544
Received (#1, 00000026) 23 bytes from 192.168.1.41-49544
00000000 2b 4f 4b 20 70 61 73 73 77 6f 72 64 2c 20 70 6c |+OK password, pl|
00000010 65 61 73 65 2e 0d 0a |ease...|
Sent (#1) to [--1]-8080
Received (#1, 0000000B) 11 bytes from [--1]-8080
00000000 50 41 53 53 20 6e 6f 6e 65 0d 0a |PASS none..|
Sent (#1) to 192.168.1.41-49544
Received (#2, 0000003D) 72 bytes from 192.168.1.41-49544
00000000 2d 45 52 52 20 5b 41 55 54 48 5d 20 6c 6f 67 69 |-ERR [AUTH] logi|
00000010 6e 20 66 61 69 6c 75 72 65 20 6f 72 20 50 4f 50 |n failure or POP|
00000020 33 20 64 69 73 61 62 6c 65 64 2c 20 74 72 79 20 |3 disabled, try |
00000030 6c 61 74 65 72 2e 20 73 63 3d 48 74 6a 4a 69 74 |later. sc=HtjJit|
00000040 63 50 52 75 51 31 0d 0a |cPRuQ1..|
Sent (#2) to [--1]-8080
Disconnected from 192.168.1.41-49544
Disconnected from [--1]-8080
Finished at 2012.04.20-19.55.17, duration 5.253979s
Двоичный лог исходящих данных log-binary-2012.04.20-19.55.17-0001-192.168.1.41-49544.log
:
USER test
PASS none
Двоичный лог входящих данных log-binary-2012.04.20-19.55.17-0001-213.180.204.37-110.log
:
+OK POP Ya! v1.0.0na@26 HtjJitcPRuQ1
+OK password, please.
-ERR [AUTH] login failure or POP3 disabled, try later. sc=HtjJitcPRuQ1
Теперь измерим производительность. Прокачаем файл напрямую, а потом через эту программу.
Качаем напрямую (файл размером около 72MB):
time wget http://www.erlang.org/download/otp_src_R15B01.tar.gz
...
Saving to: `otp_src_R15B01.tar.gz'
...
real 1m2.819s
Теперь закачаем через программу, предварительно запустив ее:
go run gotcpspy.go -host=www.erlang.org -port=80 -listen_port=8080
Качаем:
time wget http://localhost:8080/download/otp_src_R15B01.tar.gz
...
Saving to: `otp_src_R15B01.tar.gz.1'
...
real 0m56.209s
На всякий случай, можно сравнить результаты:
diff otp_src_R15B01.tar.gz otp_src_R15B01.tar.gz.1
У меня файлы одинаковые, значит все работает верно.
Теперь время. Я повторял эксперимент несколько раз (на Mac Air), и, что удивительно, закачка через программу всегда была не то, чтобы медленнее, а даже немного быстрее. Например, напрямую – 1m2.819s, через программу – 0m56.209s. Единственное объяснение, что wget
возможно работает в один поток, а программа принимает данные из локального и удаленного сокета в два потока, что может давать небольшое ускорение. Но, разница все равно минимальна, и возможно на другой машине или сети ее будет не видно, но главное, что работает как минимум не медленнее, чем напрямую, несмотря на создание в процессе передачи весьма массивных логов.
Итак, пока среди трех вариантов такой программы, на Питоне, Эрланге и Go, последняя мне нравится больше всего.
Как мне показалось, неплохой эксперимент с параллельностью в Go.
Кстати, если кто-то из джавистов замутил бы схожую программу (если можно, не требующую для сборки Eclipse/IDEA/ant/maven/spring/log4j/ivy и прочее), было бы очень интересно сравнить. И не в плане эффективности и скорости, а в плане красоты, изящности.
]]>Первое издание этой книги датировано 1982 годом. Старый код на С, даже очень хороший в прошлом, по современным понятиям чаще всего не выдерживает критики, хотя критиковать его ненужное занятие. Его стоит изучать и делать выводы.
Поэтому мне было страшно интересно, какие такие пазлы на С были в восьмидесятые. Итак, пристегните ремни. Если в программировании на С бывает адъ, то это он. Ниже я привел несколько задачек из этой книги.
Для меня – это не задачки, а кандидаты на Obfuscated C Code Context. Глядя на такой код в жизни (а такое бывает), хочется самоустраниться.
Пролистав книгу до конца (кстати, в ее конце приводятся решения всех этих так называемых задач), понимаешь все-таки ее полезность, так как плохие примеры могут служить и в добрых целях.
]]>По причине лени я начал использовать Блогспот. Тут тебе и море шаблонов, виджеты всякие, мгновенная индексация Гуглом, статистика разная, с какого-то времени даже комментарии стали древовидные, и прочие свистелки. Ну все бы хорошо, но, увы, не предназначен редактор Блогспота для создания программистских постов. Когда надо вставлять код или таблицы разные, начинаются мучения. Например, для своего другого блога, не про программирование, Яйца всмятку, сэр!, “возможностей” Блогспота вполне хватает.
Еще мне хочется хранить оригиналы постов в нормальном, не в обгаженном HTML’ем виде. Получалось, что материалы по блогу раскиданы по компьютеру там и сям в нескольких копиях. Сначала ты просто пишешь текст в редакторе, только разбивая на абзацы, без ссылок и картинок, и в конце сохраняешь почти готовый документ. Потом начинается верстка в HTML, в процессе которой, помимо, собственно, HTML’я, делаются поправки в оригинальном тексте. При этом обновлять оригинальный файл уже лень, и по сути, он остается в “сыром” виде. А в “сухом” виде остается только HTML’ная помойка. Но это еще не конец истории. Часто уже после публикации замечаешь опечатку, лезешь в Блогспот и правишь прямо на странице. Опять, самый первый оригинал и его локальная об’HTML’ная версия остаются неисправленными. В итоге: актуальные версии постов находятся только на самом Блогспоте. Конечно, можно делать автоматизированный бэкап всего блога, но опять таки – все будет уже только в HTML’е.
Некоторое время назад я начал использовать ReST. Тут жизнь хоть как-то полегчала. ReST позволяет писать текст в уже более менее предсказуемой разметке (абзацы, ссылки, код), и затем из него генерируется HTML, который вставляется (опять таки вручную) в Блогспот. Попытки автоматизировать предварительный просмотр поста через googlecl фактически провалились. Опять оставалась проблема, когда после исправления опечатки на странице оригинальный документ в ReST устаревал. Кроме того, ReST не решал проблему картинок. Их надо было куда-то заранее выкладывать, чтобы можно было полностью сделать preview.
Не могу объяснить почему, но идея динамических движков типа Wordpress’а меня как-то пугала. Сама идея держать посты в базе данных мне кажется перебором.
Я почти уже было остановился на промежуточном решении – Doku Wiki, например как на vak.ru. Тут движок хоть и динамический, но содержимое страниц хранится в файлах, и есть версионность. Doku можно использовать как движок всего сайта, не только блога. Хоть и дизайн неказистый, зато картинки и произвольные аттачменты поддерживаются системой.
Был еще вариант, на который я тоже почти подписался – блог на основе TiddlyWiki. TiddlyWiki – это мой любимый инструмент на Windows для ведения записей. Я про это уже писал. Почему только на Windows? Потому что на Маке я просто делаю записи в простых текстовых файлах, располагая их по смыслу в документах или на рабочем столе, а Spotlight, который индексирует все и вся на компьютере, моментально позволяет искать по фрагментам слов. Получается, что в ключевых возможностях TiddlyWiki – мгновенном поиске, уже не особого смысла. Но я отвлекся.
Оказывается, есть фанаты, которые превратили TiddlyWiki в блог-платформу. В эдакий статико-динамический мутант.
Например, вариант блога с таким движком – Rich Signell’s Work Log. Эзотерика, на мой взгляд. Например, не ясно, как прикрутить комментарии, хотя бы тот же Disqus. Но если кому интересно, есть даже публичный хостинг – tiddlyspot.
И вот реально я возбудился на идее чисто статических движков. Прелесть тут в том, что такой блог хостить можно где угодно. Тут не только база данных не нужна, но и серверное скриптование. Но дальше – больше. GitHub или Heroku позволяют не только хостить статические сайты, но и управлять контентом через git.
Например, есть статический движок Jekyll. В Jekyll посты пишутся с использованием разметки Markdown или Textile. Также можно добавлять в проект произвольные файлы, которые при генерации сайта будут выкладываться без изменений. По сути – это движок сайта, в котором еще можно некоторые файлы оформлять в виде блога.
Комментарии же, как основная “динамика” блога, может реализоваться через, например, Disqus. К слову сказать, есть эстэты статических блогов с высшей степенью дзэна – со статическими комментариями (для меня даже это словосочетание является оксюмороном). Подход тут такой: у поста внизу есть секция со статически выведенными ранее введенными комментариями, и рядом форма для ввода нового. Ты вводишь комментарий, и он отсылается автору блога. Тот его подтверждает (или нет), куда-то кликает, и комментарий помещается в виде файла в статический проект блога, все пересобирается и выкладывается на публику. Понятно, что это никакой ни разу не real-time, а больше похоже на комментарии с пре-модерированием, причем модератор выходит на связь раз в неделю.
Я очень ценю дискуссию, и подобный подход не для меня. И продолжаю использовать Disqus. Кстати, из Disqus можно прекрасно экспортировать базу комментариев, и, например, превратить ее в статические страницы, если вдруг придется с него уходить.
Но вернемся к Jekyll. Например, GitHub Pages напрямую поддерживает Jekyll (его автор и есть сооснователь GitHub) и умеет рендерить проекты Jekyll (хотя можно и рендерить самому локально). Заливаешь через git проект Jekyll, и сайт становится видимым в GitHub Pages.
На Heroku идея немного иная. Heroku хостит Ruby, поэтому статический сайт на Heroku – это сами страницы и программа-вебсервер, которая их отдает. Звучит страшновато, но на Ruby такой сервер выглядит весьма компактно, например так:
require 'bundler/setup' require 'sinatra/base' class SinatraStaticServer < Sinatra::Base get(/.+/) do send_sinatra_file(request.path) {404} end def send_sinatra_file(path, &missing_file_block) file_path = File.join(File.dirname(__FILE__), 'public', path) file_path = File.join(file_path, 'index.html') unless file_path =~ /\.[a-z]+$/i File.exist?(file_path) ? send_file(file_path) : missing_file_block.call end end run SinatraStaticServer
Как ни странно, хостинг на Heroku в целом проще, чем на GitHub. Также, на Heroku, git-репозиторий блога остается private, тогда как на GitHub’е он становиться открытым, как и все остальные проекты. Хотя для меня звучит странно держать проект блога (фактически, сайта) закрытым. Он же и так весь доступен через веб.
Да, и GitHub Pages и Heroku позволяют “прикрутить” нормальный домен второго уровня, если у вас есть таковой.
Итак, я выбрал Jekyll c хостингом на Heroku. Увы, если взять чистый Jekyll, то придется самому с нуля разрабатывать стили и макет страниц. Если этим заниматься лень, то можно взять Octopress.
Octopress – это статический движок блога на базе Jekyll, но который укомплектован красивым HTML5 макетом страниц, пачкой удобных плагинов и автоматизированной возможностью выкладывания блога на GitHub Pages и Heroku.
Итак, я взял Octopress, покрутил туда-сюда, попробовал несколько постов, протестировал рендеринг блога локально, повыкладывал на Heroku и GitHub Pages. Все вроде было на мази.
Далее была самая нудная часть марлезонского балета – перетаскивание постов из любимого Блогспота. Фактически приходилось это делать вручную через cut-and-paste. Недели три мучений, и свои несчастные триста постов я обработал.
Все было готово для запуска моего нового статического блога. Но тут меня ждало главное разочарование. Драгоценный Jekyll, написанный на Ruby, рендерил мои несчастные триста постов (внимание!) – 15 минут (на Mac Air). А как сами понимаете, по началу надо было много пробовать, пересобирать, снова пробовать, снова пересобирать и т.д. И такое время полной пересборки не лезло ни в какие ворота.
Методом тыка я нашел узкое место в движке Jekyll/Octopress – львиная доля этих 15 минут уходило на генерацию файла atom.xml
, RSS-фида. Почему-то в изначальных шаблонах в этот RSS-файл включалось только последние двадцать постов. Но у меня блог небольшой, поэтому я включил туда все посты, и тогда время генерации этого файла приводилось к пятнадцати минутной сборке всего блога.
Все это показалось мне каким-то абсурдом (при всей моей любви к Ruby). После небольшого размышления (я к тому времени уже более менее понимал внутренности Jekyll) и нежелания корячить Jekyll в попытках его ускорить, я задался вопросом – а не написать ли мне свой статический движок по схожей идее? Ведь это всего-навсего работа с файлами, текстом и, возможно, шаблонами. К тому же, в Jekyll нет многоязычности ни в каком виде, и у меня были планы туда ее добавить, но с собственным движком у меня полностью развязаны руки, и можно сделать все стройно и красиво.
На чем писать? Можно по-мужски: на C++/boost. Будет работать очень быстро, но скучно. Я решил на Go. Нативная, очень быстрая компиляция (фактически, у меня нет фазы компиляции, так как она совмещена с фазой запуска), удобная работа со строками и файловой системой, упрощенная работа с памятью (сборщик мусора), регулярные выражения, массивы, хэши, библиотека шаблонов, библиотека для Markdown. Все, кроме последнего, “из коробки”. Каких-либо проблем с производительностью не должно быть вообще. Тут как раз вышел релиз Go 1, и теперь есть нормальные дистрибутивы под Windows и Mac.
Итак, после трех вечеров родился мой велосипед – Goblog. Весь проект открытый. Сайт и его исходные тексты находятся вместе.
Есть два основных места: проект и собранный сайт-блог. В первом лежат исходные файлы. В процессе сборки файлы из проекта копируется в собранный сайт с сохранением локальной структуры каталогов. По умолчанию файлы копируются без изменений, как двоичные. Если же какой-то файл имеет расширение html
, xml
или js
, то этот файл прогоняется через систему шаблонов Go. Файлы с расширением markdown
дополнительно перед шаблонами обрабатываются библиотекой Markdown.
Каталоги:
<root>
– Здесь находится собранный сайт, как он видится по адресу http://demin.ws/.<root>/_engine
– Это проект, тут лежат исходники и генератор сайта. Технически, этот каталог виден и через web.Подкаталоги и файлы в каталоге _engine
:
_includes
– Файлы, которые можно подставлять через макрос {{include “filename”}}
.
_layouts
– Файлы-layout’ы (см. ниже).
_site
– Собственно, каталоги и файлы сайта. Этот каталог является корнем будущего сайта. Файлы из него при сборке перекладываются в собранный сайт. Некоторые обрабатываются шаблонами.
_posts
– Исходники постов. Эти файлы обрабатываются особо. Помимо шаблонов, они файлы переименовываются по структуре блога, где дата является частью URL: домен/blog/язык/год/месяц/день/название-поста/
Посты – это Markdown-файлы, имеющие особый заголовок и имя. Данные файлы выкладываются в отдельный каталог /blog
с подкаталогами-датами. Информация о постах собирается в специальные переменные, которые делаются видимыми из шаблонов. Также по постам строится обратный индекс для поиска.
Идея layouts унаследована из Jekyll. Если пост или страница имеет в заголовке атрибут layout
(например), то для ее рендеринга загружается указанный шаблон-layout (из каталога _layouts
), тело поста или страницы вставляется в определенное место layout’а (у меня это плейсхолдер Page.child
), и затем все рендерится вместе. Это позволяет единообразно оформлять группы схожих страниц (например, постов). Layout’ы могут быть вложенные.
И теперь, собственно, генератор – main.go.
Все, что я делаю для сборки (в каталоге _engine
), это:
make
Выводится примерно следующее:
_engine$ make
gofmt -w=true -tabs=false -tabwidth=2 main.go
go run main.go
Go static blog generator Copyright (C) 2012 by Alexander Demin
Words in russian index: 18452
Words in english index: 3563
15.672979s
Processed 344 posts.
Если все хорошо, то в корне проекта (в каталоге ..
относительно _engine
) образуются файлы, готовые для выкладки. На моем Mac Air сборка занимает 15 секунд (привет, Jekyll/Octopress, и до свидания). Tак как все находится под git, то всегда четко видно, где и какие файлы появились, исчезли или изменились.
Далее можно проверить сайт локально (см. ниже).
Если все готово, можно добавить измененные файлы (как исходники из _site/
, так и собранные файлы) в локальный репозиторий:
git add ../*
git commit -m "New post about ..."
И выложить на GitHub Pages:
git push
Практически сразу после push
файлы появляются на demin.ws.
В Makefile несколько дополнительных команд для облегчения жизни.
Чтобы запустить сайт локально, я временно добавляю 127.0.0.1 demin.ws
в /etc/hosts
и запускаю мини web-сервер. Помните, как он выглядел на Ruby? Маленький, правда? А теперь версия на Go (server.go):
package main import "net/http" func main() { panic(http.ListenAndServe(":80", http.FileServer(http.Dir("..")))) }
Итак:
go run server.go&
И можно тестировать сайт локально (возможно придется запустить через sudo
, чтобы “сесть” на 80-й порт).
В принципе, можно и не трогать /etc/hosts
и использовать адрес localhost:80
, но RSS-фид файл atom.xml
содержит абсолютные ссылки c доменом, поэтому для если надо тестировать RSS, то без подмены адреса не обойтись.
В качестве расширения Markdown у меня есть специальный тег для вставки блоков кода:
{% codeblock lang:xxx %}
...
{% endcodeblock %}
Я унаследовал этот тег из Octopress’a. Markdown уже имеет синтаксис для кода:
``` xxx
...
```
где xxx
– язык.
Но свой тег позволяет мне проще добавлять атрибуты, например, включение отображения номеров строк, преобразование табуляций и т.д.
Далее надо было решить вопрос подсветки синтаксиса. Я покрутил несколько онлайновых библиотек, которые через JavaScript раскрашивают прямо на странице, но в каждой была какая-то минимальная проблема, поэтому я таки решил раскрашивать код статически.
Первое, что пришло в голову – pygments. Все бы хорошо, но благодаря Питону, работает крайне медленно. Время полной сборки сайта с 15 секунд выросло до двух минут. Основное время тратилось на раскраску кода. Приходили мысли на тему кеша уже раскрашенных фрагментов и прочей ерунде, но после небольшого поиска проблема решилась радикально.
Надо было просто взять колоризатор, написанный на правильном для данной задачи языке. Отыскались две альтернативы: GNU Source-highlight и Highlight. Обе написаны на C++, поэтому работают практически мгновенно.
Например, вот тут человек сравнивал производительность pygments и syntax-highlight.
Мне больше понравился Highlight. В нем языков больше поддерживается (например, в GNU’шном даже Go нет). После перехода на Highlight время полной сборки вернулось к ~15-16 секундам, и я удовлетворился.
Вызов колоризатора сделан через обратный вызов в регулярном выражении, которое обрабатывает тег {% codeblock %}
(функция highlight()).
Полно редакторов с preview для Markdown. Я использую MarkdownPad под Windows, и Marked на Маке.
Я решил не делать теги вообще. Основываясь на собственном опыте, я понял, что никогда не пользуюсь тегами ни в своем блоге, ни в чужих. К тому же со временем взгляды на логику категоризации информации меняются, и порой приходится просто для совместимости с прошлым расставлять теги, в которых уже не видишь смысла. Какой, например, смысл в теге c++
в моем блоге? Кто-нибудь когда-нибудь его использовал?
Но минимализм – это не путь к усложнению жизни. Наоборот. Лично я постоянно что-то ищу у себя в блоге в старых постах. На Блогспоте я просто заходил на главную страницу, жал ⌘-F (ой, простите, CTRL-F) и искал по фрагментам слов в заголовках. Именно для этого я с некоторого в правой колонке стал выводить ссылки практически на все информативные посты.
В новом блоге все “работает” точно также прямо на первой странице с каталогом постов. При переносе постов я изменил заголовки некоторых, сделав их более информативными и пригодными для поиска.
Но! Все это уже не важно, так как теперь в блоге работает полнофункциональный контекстный поиск.
Одним из досадных неудобств Jekyll – это отстуствие каких-либо проверок чего-либо. А я прошел через это в полной мере в процессе перетаскивания постов из Блогспота. Битые ссылки, неверные даты, забытые кавычки, непроставленные языки и прочие атрибуты постов и многое другое. Поэтому Goblog везде где только можно проверяет все – форматы, ссылки, семантику и т.д. Если где-то ошибка, сборка останавливается. Когда я добавил функцию check_links(), которая проверяет все локальные ссылки по всем файлам в уже собранном сайте, я выловил изрядное количество “дохлых” ссылок.
Была еще проблема, которую, как мне кажется, удалось решить весьма элегантно: двуязычность. Мне нужен блог и сайт на двух языках. Но хардкодить “прозрачную” поддержку русского и английского как-то не хотелось, к тому же версии на разных языках могу радикально отличаться, и мне не сложно поддерживать их шаблоны независимо. В итоге, у меня есть просто понятие языка у каждого обрабатываемого файла (или поста), заданное в заголовке. Goblog не знает о языках. Он просто делает информацию о языке файла или поста доступной через шаблоны. А я уж сам решаю, где лежат какие файлы. Например, все русское лежит, начиная с корня сайта, а все английское имеет префикс /english
.
Например, русская титульная страница и английская титульная страница.
Я не люблю web-программирование: javascript, css, html, или web-дизайн, чего вообще делать не умею. Но тут мне таки пришлось покопаться в этом (с Octopress’ом было проще). Я за основу взял сайт автора Jekyll. Сделал все минималистично просто. К тому же все равно большинство людей читают через RSS и ходят на сайт только если хотят оставить комментарий. Следовательно, надо чтобы работал RSS и страничка поста была удобной (что для меня значит простой, без изощренных шрифтов и странного форматирования) для чтения.
Вы думаете, я сейчас буду убеждать использовать мой движок? Совсем нет. Хоть я старался сделать движок максимально гибким и непривязанным конкретно к моему блогу, но мне пришлось переносить старые посты и их комментарии, поддержать два языка и т.д. В итоге в коде есть куски, “заточенные” конкретно под мой блог (особенно в области Disqus-ссылок на комментарии к старым постам).
Только могу порекомендовать, это что статический движок персонального сайта/блога можно написать самому. Почему? А потому, что эта задача решается за несколько вечеров (раз), и в нем будет только то, что вам реально нужно (остальное вам будет лень программировать) (два). Уверен, что все можно было сделать и на Руби, и на Питоне, PHP и т.д. Но было глупо упускать возможность поупражняться на новом языке с реальной задачей.
■
Ссылки по теме:
P.S. Этот писался почти неделю, урывками. Параллельно я писал поиск. Внезапно я осознал, как все-таки это нереально удобно с git’ом работать с блогом. Пишешь в бэкграунде пост – работаешь в одной ветке, дописываешь функционал – другая ветка. Когда что-то готово, сливает в master и push на GitHub. Красота.
]]>Но были еще несколько книг, от которых у меня нежно вибрирует что-то внутри. Не потому, что все книги были особенно хорошие. Просто они были первыми. Про них можно сказать, что я их зачитал до дыр.
Некоторые позже переиздавались, но я привожу их первые издания.
Книги приводится в хронологическом порядке, по мере взросления, так сказать. Удивительно, я до сих пор помню очень многое из этих книг.
Зеленко Г.В., Панов В.В., Попов С.Н., “Домашний компьютер”, 1989
Брат собирает Радио-86РК, а эта книга является странным руководством по всему: основам микроэлектроники, программированию для КР580 (Intel 8080) в машинных кодах и ассемблере, и под занавес - на Бейсике.
У меня есть теория, что то, что человек плотно изучает в возрасте от 9-14 лет, настолько крепко врезается в мозг, что удалить это практически невозможно. Иначе как можно объяснить, что до сих пор помню машинные коды Intel 8080 со времен Радио-86РК, но не могу вспомнить, что делает код, который я писал месяц назад.
Г. Х. Геворкян, В. Н. Семенов, “Бейсик - это просто”, 1989
Практически никакая книга по содержанию, но пару лет назад я купил ее оригинальное издание. Хотелось снова подержать его в руках.
В. П. Дьяконов, “Справочник по алгоритмам и программам на языке бейсик для персональных ЭВМ”, 1989
Не могу сказать, чтобы я много понимал в этой книге тогда. Но в ней было много много примеров, на различных диалектах Бейсика.
Мымрин М. П., “Конструкция, применение, программирование и ремонт ПЭВМ “Агат”, 1990
Морер У., “Язык ассемблера для персонального компьютера Эпл”, 1987
Эта книга была иконой и наверное единственным доступным тогда руководством по программированию на ассемблерe для 6502. Храню до сих пор.
В. Э. Фигурнов, “IBM PC для пользователя”, 1990
А кто не читал эту книгу? Хотя бы одно из изданий? В первом издании, что на картинке, между прочим, был исходник самодельного “антивируса”, написанного на Турбо-Паскале 5.0, который сканировал все файлы на жестком диске и запоминал их контрольные суммы.
В. В. Фаронов “Программирование в среде Турбо-Паскаль 5.0 на персональных ЭВМ”, 1990
Мега-книга своего времени. Описание библиотеки Турбо-Паскаля (а где его еще было взять?), много довольно низкоуровневых примеров (например, работа с экранной областью из Паскаля!). Этой книги у меня долго не было в личном пользовании, поэтому я переписывал ее фрагменты вручную.
Гербер Шилдт, “Язык СИ для профессионалов”, 1989
Очень много нового: алгоритм отрисовки отрезка без использования вещественной арифметики, отрисовка окружности без синусов и косинусов, интерпретатор Бейсика, программирования аппаратного таймера и многое другое.
П. Нортон, Д.Соухе, “Язык ассемблера для IBM PC”, 1993
После этой книги понял, что на ассемблере можно писать большие вещи.
С.Н. Баранов, Н.Р. Ноздрунов, “Язык Форт и его реализации”, 1988
Форт - это срыв мозга. Удивительный язык. И книга отличная.
П. Хижняк, “Пишем вирус… и антивирус”, 1991
Это был для меня реальный хит, крупицы “хакерского знания” в форме доступной книги. Естественно, вирус… и антивирус были запущены на школьный Robotron.
Вроде все.
Кто поделится своими историями?
]]>Случайно наткнулся где-то в твиттере на эту книгу. Несложный поиск дает множество вариантов c ней ознакомиться.
Покупать для себя я бы не стал. Для более менее опытного программиста на С тут нет ничего нового. Но, как всегда это бывает, имеет смысл за часик, под чай или кофе, пролистать и освежить в памяти разнообразные причуды указателей, моделей памяти (вечное здоровье сегментированной особым образом модели памяти родом из 8086 и 80286), ABI, особенности линковки в UNIX и Windows, ну и факты из истории языка и разнообразных его редакций.
В этой книге всего понемногу.
P.S. Кстати, я думал, что знаю C потенциально лучше чем С++, просто потому, что язык объективно меньше и проще. Да и писать на нем я начал раньше. И решил я как-то сдать тест по С на Брейнбенче. Оказывается, есть столько аспектов языка, которые я раньше просто не встречал.
Понимаю, Брейнбенч - всего лишь тест, во многом далекий от реальности, но все равно позволяет расширить, так сказать, рамки понимания самого себя.
]]>vector
. Вопрос: как вернуть этот экземпляр вызывающему?
Правильное с точки зрения логики и стройности программы решение выглядит так:
std::vector<int> create_vector(const size_t N) { std::vector<int> v; v.resize(N, 0xDEADC0DE); return v; }
Тут экземпляр вектора возвращается по значению, что означает потенциальное глубокое копирование локального объекта в контекст вызывающей функции. Сразу возникает сомнение: а что, если вектор огромен - его ж надо будет побайтно перекладывать из одного места в другое? Гораздо “разумнее” было бы написать:
void create_vector(const size_t N, std::vector<int>* v) { v->resize(N, 0xDEADC0DE); }
Тут вектор передается по указателю, и стопроцентно ненужного полного копирования не будет. Но такой код выглядит откровенно плохо.
Сравним скорости работы на векторе длиной 100MB. Например, на компиляторе:
Apple clang version 3.1 (tags/Apple/clang-318.0.45) (based on LLVM 3.1svn)
Target: x86_64-apple-darwin11.3.0
По значению:
#include <iostream> #include <vector> std::vector<int> __attribute__((noinline)) create_vector(const size_t N) { std::cout << "by value" << std::endl; std::vector<int> v; v.resize(N, 0xDEADC0DE); return v; } int main(int argc, char* argv[]) { for (size_t i = 0; i < 10; ++i) { const size_t N = 1024 * 1024 * 100; std::vector<int> v = create_vector(N); if (v[i] != 0xDEADC0DE) { std::cout << "Test is rubbish" << std::endl; return 0; } } return 0; }
Запускаем:
clang++ -O3 -o by_value by_value.cpp && time ./by_value
Результат:
0m4.933s
Теперь по указателю:
#include <iostream> #include <vector> void __attribute__((noinline)) create_vector(const size_t N, std::vector<int>* v) { std::cout << "by pointer" << std::endl; v->resize(N, 0xDEADC0DE); } int main(int argc, char* argv[]) { for (size_t i = 0; i < 10; ++i) { const size_t N = 1024 * 1024 * 100; std::vector<int> v; create_vector(N, &v); if (v[i] != 0xDEADC0DE) { std::cout << "Test is rubbish" << std::endl; return 0; } } return 0; }
Запускаем:
clang++ -O3 -o by_pointer by_pointer.cpp && time ./by_pointer
Результат:
0m4.852s
Время в обоих случаях одинаково. Получается, что стоит выбрать первый, “красивый” вариант.
Объяснений тут два. Первый, и возможно самый важный - это RVO, Return value optimization. Это когда компилятор догадывается, что создаваемый локальный экземпляр вектора предназначен для возврата из функции, и сразу создает его в контексте вызывающего кода, чтобы потом не копировать туда. Фактически компилятор реализует передачу по ссылке, но неявно, не портя красоту исходного кода. Данный трюк будет работать для любого класса, не обязательно класса из STL.
Но оптимизация - это негарантированная вещь. Но тут есть еще одно подспорье. Стандартные контейнеры STL реализованы так, что при даже при глубоком копировании фактически копируется только небольшая управляющая структура, а сами данные, размещенные в куче, просто перебрасываются указателем, без их фактического перемещения. Тут, конечно, будет небольшое дополнительное копирование, но оно минимально, и возможно на него стоит пойти ради сохранения красивого кода.
Ну а в контексте C++11, где есть семантика перемещения, вообще не будет лишних копирований, если класс “правильно” реализован (что верно для классов из STL).
Мораль: используйте по возможности контейнеры из STL и оставьте оптимизацию компилятору. Иногда, конечно, компилятор ошибается, но таких случаев гораздо меньше, чем наоборот.
]]>
Про написание кода написано много хороших книг, поэтому, когда товарищ прислал мне ссылку на еще одну, новую, я был настроен скептически. Но удивительно, прямо в первой главе начали обсуждать следующее:
Стиль изложения краткий и конкретный. Я даже был удивлен, как они меньше чем за двести страниц хотят раскрыть тему “искусства”. В итоге, я купил книгу, чтобы узнать это.
Если честно, я не пожалел несколько часов, потраченных на чтение. Я не нашел там откровений, но вот как четкое и конкретное пособие для новичков - это книга является неплохим сборником “делай раз, делай два”. Без теории, на примерах обсуждается:
Также вкратце обсуждалось unit-тестирование.
Авторы не просто говорят, что хорошо, что плохо, но и на примерах показывают, как можно улучшать код. В конце книги рассматривается реальная задача - класс, подсчитывающий трафик в сети за последний час/день.
Сначала дается простая реализация, и затем приводятся еще две версии, в которых можно увидеть, как можно (или нужно) подходить к компромиссу между эффективностью и читабельностью.
Лично мне эта задача показалась очень подходящей для подобного объяснения.
Итак, если есть возможность купить эту книгу на работу, лишней она не будет. Ну а если для дома, для семьи лучше купить что-нибудь более фундаментальное.
]]>Можно сделать:
void f(int a, int /* b */) { ... }
Но это как-то некрасиво.
Лучше сделать так:
#define DISCARD_UNUNSED_PARAMETER(x) (void)x void f(int a, int b) { DISCARD_UNUNSED_PARAMETER(b); ... }
Такое объявление наглядно, и можно легко найти все такие места в проекте.
Кстати, компилятор Go считает ошибкой, когда обнаруживается неиспользуемая переменная или подключен неиспользуемый модуль. Сначала это напрягает, так как при экспериментах ты так или иначе постоянно что-то добавляешь, убираешь, комментируешь, возвращаешь назад и т.д. При этом, конечно, появляются такие переменные и модули, и их надо постоянно исправлять. Но в итоге, такой подход не позволяет в коде оставаться мусору, который ты забыл убрать после экспериментов (ну кому охота чистить список подключенных, но возможно неиспользуемых модулей STL, например?).
]]><script language="javascript"> (function() { function async_load(){ var s = document.createElement('script'); s.type = 'text/javascript'; s.async = true; s.src = 'URL скрипта для загрузки'; var x = document.getElementsByTagName('script')[0]; x.parentNode.insertBefore(s, x); } if (window.attachEvent) window.attachEvent('onload', async_load); else window.addEventListener('load', async_load, false); })(); </script>
Говорят, что это модный HTML5 совместимый способ. Идиома, так сказать.
]]>Почему?
Я сам очень часто ищу старые посты в своем блоге, например, когда пишешь новый пост. Раньше я просто заходил на главную страницу и делал CTRL-F по фрагментам заголовков (видели, что практически все информативные посты были выведены заголовками в правую колонку?). Это было не совсем удобно, но тегами я все равно не пользовался никогда. Да и на чужих блогах я крайне редко использую теги. Разве чтобы через “облако тегов” понять круг интересы автора, но и только. Кроме того, теги имеют тенденцию “протухать”. Однажды не очень правильно присвоенный тег приходится поддерживать. Ну вот какой смысл тега “c++” у меня блоге?
Можно еще предположить, что кто-то захочет подписаться на feed-ленту конкретного тега, но, опять таки, у меня маленький блог, постов и так немного, и вероятность такого крайне мала.
В общем, сделаем по-гугловски: Don’t sort. Search!
На странице каталога блога теперь есть поиск (справа).
Это контекстный поиск по словам всех постов. Индексируются заголовки и сами посты. Поиск работает через простейший обратный индекс. Сортировки по релевантности нет - выводятся все посты в стандартной сортировке по дате создания, где встречаются введенные слова.
Попробуйте, например, ввести “putenv” в строку поиска, или “erlang”, а потом добавить еще слово “tcp”. Если задано несколько слов в поиска, ищутся посты, где встречаются все слова. Слова, короче трех символов, игнорируются.
По мне - так очень удобно!
Проверено в Chrome и Safari на Маке.
]]>Сегодня на повестке дня тема употребления герундиев и инфинитивов после глаголов. Например, как правильно:
“He intended to finish it.” или “He intended finishing it.”
или
“He recommended to finish it.” или “He recommended finishing it.”?
Как это обычно бывает, в данном случае работает рекомендация из фильма моего детства “Москва-Кассиопея”, звучащая так: “чего тут думать, тут знать надо!”
Брат нагуглил как-то небольшую подборочку-справочник (см. ниже), которой лично я регулярно пользуюсь при написании чего-либо на английском.
Увы, сам я “не чувствую” как правильно и, главное, почему. Если кто-то обладает способом это понять, а не запомнить - буду очень признателен.
Одной из проблем, связанных с выбором точного английского эквивалента, является согласование некоторых глаголов с герундием и инфинитивом. После некоторых глаголов можно использовать только инфинитив, после некоторых — только герундий, некоторые же глаголы допускают после себя использование и инфинитива, и герундия.
Инфинитив используется, как правило, после следующих глаголов:
Примеры:
We hope to buy a car. – Мы надеемся купить машину.
You expected to get all information you need. – Ты ожидал, что получишь всю необходимую тебе информацию.
He doesn’t want to produce these goods. – Он не хочет производить эти товары.
You want to use a new computer. – Ты хочешь использовать новый компьютер.
He deserves to be sent to prison. – Он заслуживает того, чтобы его отправили в тюрьму.
После следующих глаголов, как правило, используется герундий:
Примеры:
He enjoys talking to you. – Он получает удовольствие от разговоров с тобой.
They postponed leaving. – Они отложили отъезд.
They stopped firing. – Они прекратили стрельбу.
He regrets being unable to help. – Он сожалеет, что не мoжет помочь.
]]>В статье реалиазация на Ruby. Так как в Ruby можно опускать скобки вокруг параметров при вызове функций, то сейчас целевой код выглядит ну почти на настоящий ассемблер. И не скажешь, что это Ruby.
Посты по теме:
]]>void Test_SplitPair() { typedef std::pair<std::string, std::string> Pair; using string::SplitPair; const Pair p1 = SplitPair("", '='); assert(p1.first.empty()); assert(p1.second.empty()); const Pair p2 = SplitPair("=", '='); assert(p2.first.empty()); assert(p2.second.empty()); const Pair p3 = SplitPair("name=value", '='); assert(p3.first == "name"); assert(p3.second == "value"); const Pair p4 = SplitPair("name = value", '='); assert(p3.first == "name"); assert(p3.second == "value"); const Pair p5 = SplitPair(" n ame \t = va lue \r\n", '='); assert(p5.first == " n ame \t "); assert(p5.second == " va lue \r\n"); }
Как его можно улучшить? Например, разбить на отдельные тесты. Это идеальный вариант. Но можно сделать и так (что лично на мой взгляд не так уж и плохо), чтобы решить проблему копипаста новых примеров:
void Test_SplitPair() { typedef std::pair<std::string, std::string> Pair; using string::SplitPair; { const Pair p = SplitPair("", '='); assert(p.first.empty()); assert(p.second.empty()); } { const Pair p = SplitPair("=", '='); assert(p.first.empty()); assert(p.second.empty()); } { const Pair p = SplitPair("name=value", '='); assert(p.first == "name"); assert(p.second == "value"); } { const Pair p = SplitPair("name = value", '='); assert(p.first == "name"); assert(p.second == "value"); } { const Pair p = SplitPair(" n ame \t = va lue \r\n", '='); assert(p.first == " n ame \t "); assert(p.second == " va lue \r\n"); } }]]>
В моей версии это было “Семь языков за семь вечеров”. Для каждого языка дается минимальное введение, которое имеет смысл только если язык для вас вообще новый. Еще приводятся мини интервью с создателями языков, и один из интересных задаваемых им вопросов - это “чтобы вы сделали в языке иначе, если б можно начать сначала”.
Описываются языки:
Обзор каждой главы - это мой субъективный взгляд на две вещи сразу: язык программирования и материал главы про него. Объясню почему - для знакомых языков вряд ли имеет смысл описывать сам язык. Может имеет смысл отметить интересные отличительные моменты. А вроде для неизученных, типа Пролога или Clojure, можно и остановиться немного на самом языке.
Ruby
Про Ruby ничего особенно из книги не вынес, так как вдумчиво читал “Programming Ruby 1.9”, после чего подсел на этот язык. Ruby - фантастический скриптовой язык. Каждый раз, когда пишу на нем, испытываю удовольствие примерно такое, когда я после Perl’а попробовал в первый раз PHP.
Автор языка сказал в интервью, что, создавая бы язык заново сегодня, он бы хотел для многопоточности вместо традиционных потоков сделать модель actor.
В двух словах, Actor - это когда параллельные потоки разделяют ресурсы не через память и механизмы синхронизации типа мьютексов и семафоров, а через обмен сообщениями, прием и посылка которых обеспечиваются средой, и они встроены в синтаксис языка. Например, как в Scala, Go, Erlang, Io.
Io
Io очень компактный, на мой взгляд эзотерический язык, основанный на прототипах, как JavaScript, когда нет четкого разделения между классами и объектами. Минимальный и очень простой синтаксис.
Интересный механизм многопоточности в дополнение к actor и coroutine (коллективная многозадачность, как в Coroutines в Lua), называемый futures. “Future” - это вроде бы как обычный actor, поток запущенный работать параллельно. Но с одним отличием: как только создающий поток попытается воспользоваться результатом future, он будет заблокирован до тех пор, пока future не вычислит это значение.
Примерчик из книги:
// Запускаем future futureResult := URL with("http://google.com/") @fetch writeln("Сразу начинаем делать что еще, пока future работает в фоне.") // Эта строка будет выполнена сразу. writeln("fetched ", futureResult size, " bytes") // А вот эта строка будет заблокирована, пока future не выполнится.
Идем дальше, Prolog.
Этого зверя я грызу давно. К счастью, благодаря освоению Erlang’а, я стал реально въезжать в функциональную тему в целом, и монстры типа Пролога или Хаскелла уже не за пределами понимания.
Так совпало, что глубина материала по Прологу легла точно для моего уровня. Задача восьми ферзей и поиска решений Судоку были для меня отличными примерами.
В двух словах: программа для Прологе - это набор фактов и связей между ними. Затем Пролог, выполняя программу, поиском в глубину обходит пространство решений и выбирает те, которые удовлетворяют всем заданным фактам и связям между ними.
Фактические программа поиска решения Судоку - это набор переменных, составляющих клетки поля Судоку, и набор правил - разнообразные суммирования по группам, по строками и столбцам (по правилам Судоку). И затем Пролог перебором ищет подходящие значения и комбинации переменных.
Конечно, это очень поверхностный взгляд, но который лично мне добавил много понимания.
Идем дальше, Scala.
Отмечу только отдельные факты, интересные мне.
Многопоточность на основе actors, то есть когда потоки обмениваются сообщениями. После Go и Erlang понимаешь как это удобно и правильно.
Про остальное - по-моему в Scalа есть все возможные свистелки и перделки, когда-либо придуманные в области языков программирования. В общем, если вы фанат Java VM, то надо брать полноценную книгу по Scala и грызть ее.
Идем далее, Erlang.
Тут тоже скажу мало, так как я фанат этого языка, и уровень этой книги мне был мал, но введение дается хорошее для ознакомления с функциональной сутью Erlang’а и его моделью многопоточности.
Clojure
Снова язык на основе Java VM. Clojure - это разновидность Лиспа со всеми вытекающими.
Интересная возможность языка, в общем-то не связанная с его лисповой сущностью - это STM, software transactional memory. Это когда некий кусок кода в программе объявляется транзакцией, и он выполняется атомарно, либо все изменения откатываются.
Ну и под занавес, Haskell.
Хаскелл суров, и данная книга - это крайне минимальное введение, просто для запоминания слова Хаскелл. Я кое как осилил отличную книгу Душкина и “Programming in Haskell”, а сейчас читаю “Real World Haskell”, поэтому главу этой книги просто пролистал.
Вывод: книга 100% одноразовая, но, как говориться, раз не… полезно для кругозора и для программистских терок на кухне.
]]>Просто хотел поделиться текущими интересами.
Каждодневная работа (проектирование, планирование, кодирование и ревью): C и С++. Тут C++ 0x11 полным ходом, и надо подтягиваться. ACCU 2012 в этом году посвящен в основном новому C++.
Для души:
Для “погрызть” в надежде когда-нибудь написать что-нибудь реальное - Haskell и немного Пролог (тут надежд совсем мало).
В очереди на хотя бы минимальное ознакомление: Clojure. Тут надежд больше, так как это все-таки Lisp.
С общей недавней миграцией на Мак хочется попробовать в действии Objective-C и AppleScript. А что еще писать на Маке, да еще и на Objective-C? Конечно UI! А UI это на 200% не мой профиль. Но, если серьезно, сложно загасить внутреннюю хотелку освоить Objective-C, когда тут вокруг всякие iOS’ы.
Из недавнего ознакомленного, но в которое не втянулся:
Итак, связи зафиксированы. Теперь их надо их как-то представить и построить граф, визуально.
Вроде не самая тривиальная задача, но оказывается, решается весьма просто.
Есть такой язык представления графов, называется DOT. Прелесть его в предельной простоте. Например, простейший граф:
graph name {
a -- b
b -- c
b -- d
}
Натравливаешь на это дело специальную программу и получаешь:
Все! Картинка на выходе в SVG. Можно хоть на стену вешать.
К сожалению, лучший софт, что я нашел для визуализации DOT - это Graphviz. Вроде и работает неплохо, строя весьма большие графы, и есть на всех платформах, благодаря Java, но по интерфейсу - это запредельный и неописуемый кусок говна. Увы.
Если кому интересно, я выложил пример реальной трассировки (по понятным причинам, имена изменены). В целом дает представление о простоте исходника и о возможностях визуализации - PNG и SVG.
Повторюсь - процесс формализации графа крайне прост - нужно только задать пары связанных вершин. Можно делать направленные графы, можно задавать вершинам и дугам разные атрибуты.
В целом, отличная технология.
]]>Это микрокомпьютер на базе Microchip PIC32 со встроенным Бейсиком. Прелесть тут в том, что собрать его можно за пару часов.
По возможностям он немного мощнее Радио-86РК и классического Спектрума. Но вот периферия у него сказочная: SD/FAT карточка, USB, VGA, PS/2, таймеры, RS232, I2C, SPI, PWM, ADC/DAC и просто одиночные порты-пины общего назначения.
Если собирать на макетной плате, то цена будет, по заявлению автора, менее десяти австралийских долларов.
Проект полностью открытый. Автор дает схемы, исходные коды прошивки и рекомендации по наладке.
Если даже быстро пролистать документацию, видно, возможностей прорва. Можно практически на коленке создавать различные мини-контроллеры чего угодно. Работа со всей выше перечисленной периферией ведется прямо из Бейсика.
Программы и данные можно хранить на SD карточке. Если на карточке есть файл AUTORUN.BAS
, то прошивка автоматически запускает его при старте.
Мне это все понравилось, но паять мне было лень. А в интернете продавались только конструкторы.
В итоге я заказал конструктор у Altronics.
И вот он пришел. На плате запаян только микропроцессор, ибо для пайки такого корпуса надо либо иметь паяльную станцию, либо большое умение.
Поехали.
Вот тут я уже припаял несколько элементов. Я в пайке не совсем новичок, но держал паяльник в руках последний раз лет пять назад. Кислоты у меня не было, поэтому для ускорения процесса я выкрашивал канифоль прямо на точки пайки. Эффект примерно такой же. Паяльник (тот, что в тарелке) с острым жалом.
Первый час я возился с несколькими элементами, но потом дело наладилось.
Вот тут уже готова половина.
Но еще через час все было готово.
Maximite может питаться либо от внешних 9 вольт, либо от USB. Я подключил вторым способом.
Итак, запуск. Подключаем USB и VGA к монитору. Работает!
Бейсик готов выполнять команды, но пока нет клавиатуры. Чисто PS/2 клавиатуры у меня не было, поэтому я попытался через USB-PS/2 переходник. Увы, воткнуть не получилось.
На следующий день я взял у наших айтишников старую PS/2 клавиатуру и таки подключился.
Корпус.
В закрытом виде.
Надо отдать должное - конструктор от Altronics отличного качества. Отверстия на плате металлизированы, что значительно упрощается пайку. Корпус моментально собирается.
Теперь надо было обновить прошивку, так как автор проекта уже успел ее значительно улучшить с момента выпуска конструктора.
Maximite имеет встроенную возможность обновления прошивки, и специальный программатор не нужен. Надо открыть корпус и перезапустить Maximite, удерживая специальный микро-выключатель. Устройство впадает в состояние boot loader’а, и специальной утилитой через USB можно заливать обновление.
Maximite видится в USB-подсистеме как стандартное CDC устройство. Но для Windows нужно все равно сначала поставить драйвер для создания виртуального COM-порта. На Маке этот драйвер встроен.
Подключаем.
Заливаем.
Ура. Прошивка обновлена с 2.1 до последней 3.0A.
Как я уже говорил, Maximite поддерживает VGA для дисплея и PS/2 для клавиатуры. Но это не все. Если подключить Maximite через USB к компьютеру, то кроме питания можно запустить программу эмулятор терминала, которая через виртуальный порт RS232 (работающий через USB) может обмениться данными с Maximite. Все, что Maximite выводит на VGA также дублируется в порт, а все что Maximite получает из порта расценивается как принятое с клавиатуры.
То есть можно вообще отключить VGA и PS/2 и работать чисто через терминал. Это офигительная возможность.
Например, картинка с VGA (вольтметр):
И одновременно с экрана терминала:
Забавно, экран у Maximite работает с точками, а не со знакоместами. Когда на экран выводится символ, то он дублируется, как я уже сказал, в терминале. А если рисуется графика, то она, естественно, в терминале не видна.
Диалект Бейсика в Maximite немного необычен, но зато дает доступ ко всей периферии без ограничения, причем прямо операторами языка.
На сайте автора есть архив с программами на Бейсике, демонстрирующие некоторые возможности Maximite.
Я приведу несколько картинок.
Часы.
Редактор знакогенератора.
Вольметр.
Пару головоломок.
А что это, думаю, объяснять не надо.
Ну, конечно, привет Хабру!
Заключение
Каждая копейка, потраченная мной на этот эксперимент, стоила полученного кайфа.
Сам проект Maximite удивляет своей законченностью. Все как-то очень органично и просто. И самое главное - это работает!
Как мне кажется - для начинающих, даже детей, интересующихся микропроцессорной техникой, Maximite - это просто находка. Элементарная сборка, не требующая настройки. Я, как полный дилетант, собрал все за несколько часов.
Когда мой брат лет двадцать назад собирал Радио-86РК и Спектрум, ходила шутка про устройства для самостоятельной сборки, описываемые в журнале “Радио” - если авторы говорят, что устройство не требует наладки, то значит есть хотя бы минимальный шанс его наладить, ну я если авторы говорят, что требуется минимальная наладка…
В общем, хотите тряхнуть восьмибитной стариной с паяльником в рукам - соберите Maximite.
]]>Пока есть вот такая идея - для каждого процесса, находящегося под контролем, периодически делать извне снимок стeка. Например:
#0 0x991a8c22 in mach_msg_trap ()
#1 0x991a81f6 in mach_msg ()
#2 0x968870ea in __CFRunLoopServiceMachPort ()
#3 0x96890214 in __CFRunLoopRun ()
#4 0x9688f8ec in CFRunLoopRunSpecific ()
#5 0x9688f798 in CFRunLoopRunInMode ()
#6 0x92158a7f in RunCurrentEventLoopInMode ()
#7 0x9215fd9b in ReceiveNextEventCommon ()
#8 0x9215fc0a in BlockUntilNextEventMatchingListInMode ()
#9 0x90010040 in _DPSNextEvent ()
#10 0x9000f8ab in -[NSApplication nextEventMatchingMask:untilDate:inMode:dequeue:] ()
#11 0x9000bc22 in -[NSApplication run] ()
#12 0x902a018a in NSApplicationMain ()
#13 0x0012e356 in main ()
Для Linux, HPUX и Solaris есть команда pstack
, для AIX - procstack
. Уверен, что и для Windows можно подобное замутить. Process Explorer это умеет делать, значит можно сделать и программно.
Сравнивая текущий снимок с предыдущим, можно понять, насколько он изменился. Если он вообще не изменился или изменился только по некоторым начальным функциям (например, внутри ядра), то можно предположить, что процесс «завис». Случай deadlock’а на файле или базе данных еще проще, так как программа просто будет постоянно торчать на одной функции внутри ядра.
Понятно, что сравнение стеков должно все-таки учитывать особенности проверяемых процессов. Можно сделать его конфигурируемым, например, через регулярные выражения или скриптовой язык типа Lua.
Тема еще в том, что такой монитор не требует изменения в самом софте, и может быть написан на любом языке, например Руби или Питоне. По сути это просто работа с текстовыми данными.
Может я изобретаю велосипед?
]]>void foo(T* t) { bar(t); }
Проблема в том, что функция bar является legacy-функцией одной из наших старых библиотек, которую сейчас мы поменять не можем, и ее сигнатура: void bar(T*)
, то есть указатель-параметр не const
. Но в реальности эта функция не меняет объект, на который указывает ее параметр.
Далее. Наш новый API, частью которого является foo
, есть новейшая разработка, и должна быть спроектирована по уму. С точки зрения запланированной ответственности функции foo
, она не должна менять объект, на который указывает t
.
Я считаю, что код должен выглядеть так:
void foo(const T* t) { bar(const_cast<T*>(t)); }
Мои аргументы: так как контракт функции foo говорит, что эта функция не будет менять объект, на который указывает указатель, то этот факт должен быть отражен в API использованием слова const
. И не имеет никакого значения, что по какой-то причине реализация этой функции внутри использует старый код, который неграмотно написан. Да, из-за это приходится делать некрасивое приведение типов, снимая const
. Но эта некрасивость локализована внутри foo
и в целом не оказывает влияния на стройность нового кода. Более того - если в будущем можно будет отказаться от использования старой функции bar
, то проблема вообще исчезнет.
А вот контр-аргумент коллеги: может так случиться, что из-за ошибки в bar
константный аргумент функции foo, которая по идее не должна менять аргумент, будет таки изменен, и получится крайне неприятный баг. В итого надо сделать аргумент функции foo
НЕ const
(и приведение будет уже не нужно), тем самым явно показать конечному пользователю нового API, что не стоит вообще рассчитывать на константность параметра функции foo
.
Мы так и не договорились, так как у меня был «прогиб» в плане возможного нарушения константности вне зависимости, что там есть красивый const
, а в подходе коллеги было не ясно, как объяснить в документации по функции foo
почему и как она может менять свой аргумент - сказать, что мол из-за особенностей реализации foo
мы не можем гарантировать константность? Получается, что мы проблемой старого кода портим дизайн нового API.
Дилемма.
P.S. Есть еще один изотерический вариант: внутри foo
делать глубокое копирование объекта T
и уже его передавать по неконстантному указателю в bar
. Лично я, если надо выбирать между быстрым, но «плохим» кодом, и медленным, но со стройным дизайном, чаще выбираю второе, так как завтра купленный более быстрый сервер ускорит хороший, но медленный код, но не сделает плохой код более понятным.
Я думал, что биографии читают только “старые люди”, но эта книга доказала мне обратное. Поддавшись на рекламу, и ведомый моим недавно начавшимся увлечением продукций Apple, я начал читать.
Cначала это было похоже на пересказ фильма “Пираты силиконовой долины”, но через пару глав я уже не мог оторваться.
Можно говорить, что это конъюнктура и попытка сыграть на ситуации, что Стив только что умер, но, несмотря на все эти предрассудки, я подсел практически сразу.
Уж не знаю, в чем тут дело, может просто автор мастер своего дела, но я так и не оторвался, пока не прочитал до конца.
Вывод: настоятельно рекомендую, даже для принципиальных нелюбителей Эппла.
]]>Из недавнего.
“The world is flat 3.0: A Brief History of the Twenty-first Century”, Thomas L. Friedman
Убийственная книга для осознания, что:
И многое многое другое.
Не буду скрывать, были моменты в жизни, когда я думал, что занимаюсь чем-то не тем (хотя профессию я не выбирал, она сама меня выбрала), и лучше бы мне работать, например, в области недвижимости, природных ресурсов, рекламы или медиа. Если у кого-то есть хоть минимальные подобные сомнения - это книга вытрет их навсегда и, возможно, подскажет путь.
Это очень интересное и познавательное чтиво.
]]>Сначала мне показалось, что такой код не должен компилироваться, так как если функция private, она недоступна для использования в дочерних классах. Наблюдался какой-то очередной пробел в моих знаниях по C++.
Я написал программу:
#include <iostream> class A { public: void bar() { foo(); } private: virtual void foo() = 0; }; class B: public A { private: virtual void foo() { std::cout << "B::foo()" << std::endl; } }; int main(int argc, char* argv[]) { A* a = new B(); a->bar(); delete a; return 0; }
И VS2010 и GCC прекрасно его съели, и программа печатает “B::foo()“.
Напрашивание такое объяснение - механизм виртуальных функций (технически переопределение функций через vtable) - это runtime, а public/private - это compile time. Получается, что в compile time все законно, и разделение на private/protected/public не зависит от виртуальности функции, а в runtime класс B просто подставляет другую функцию через vtable уже вне зависимости от private/public.
]]>Артемий Лебедев как-то отлично сказал, что презирает всякую хрень типа SEO, так как надо создавать интересный и читаемый контент, а не суетиться по мелочи на его раздаче. Поэтому его ЖЖ использует стандартный шаблон без каких-либо попыток выглядеть “клевым”. Но его контент делает блог одним из лидеров в российском прокате.
Да и кого сейчас волнует дизайн блога? Все равно подавляющее количество читателей видят твой блог через призму Google Reader’а.
Но оставим одиозного Лебедева и вернемся к нашим специализированным техническим блогам. Как написал Том Престон-Вернер, один их основателей Github, что ведя блог, он хочем писать посты, а не подкручивать там и сям шаблоны и прочие техдетали всяких WordPress’ов и Mephisto’в.
Подобная же мысль мучает меня с первого дня использования Блогспота. Только недавно я более менее устаканил процесс написания постов, используя ReST. Сейчас я пишу и храню посты в этой разметке, а перед публикацией прогоняю через пару доморощенным скриптов типа “ReST > HTML > Фильтрованный Для Блогспота HTML”. Хотя проблема хостинга картинок все равно решается вручную заранее.
Уже сотни раз я подумывал о собственной платформе на том же WordPress’е, но природная лень и нежелание тратить ни секунды на администрирование всегда останавливало.
Сейчас у меня очередной приступ ненависти к Блогспоту и, как следствие, исследование альтернатив.
Удивительно, но как-то никогда не задумывался, что блог может на быть статическом движке.
А что, если переехать на GitHub Pages? Там суть в том, что один из твоих репозиториев может стать вебсайтом, который работает на статическом движке Jekyll.
Сразу убиваются несколько зайцев. Работаешь в нормальной разметке и используешь git для выкладывания постов. Пишешь и отлаживаешь пост локально. Затем “git push origin master” и все – пост выложен. Текст можно писать не в идиотском HTML, а в Markdown или Textile. ReST, конечно, круче, но это не смертельно.
Более того GitHub Pages позволяет интегрироваться с полноценным доменом второго уровня.
Далее тема комментариев. Не то, чтобы программировать свой движок для комментирования, а даже просто настроить WordPress – лично у меня нет никакого желания. Поэтому одна их самых простых альтернатив – Disqus.
Анализ посещаемости решается через Google Analytics.
Что остается?
Иногда люди таки ходят на сайт блога напрямую – для поиска старых постов или узнать об авторе что-нибудь социальное. Вот тут, конечно, всякие примочки типа гугловских гаджетов – поиск, каталог по датам, интеграция с социальными сетями и т.д. – имеют смысл. На Блогспоте ты их добавляешь кликом мышки, а для собственного блога надо будет тратить реальное время.
Но, возвращаясь к вопросу – а нужны ли все эти гаджеты? Если контент в блоге интересен – он найдет своего читателя. Сарафанное радио в виде твитера или фейсбука приведет тебе читателей, если будет что читать.
А если уж говорить на “раскрутке” – та же публикация удавшихся постов на Харбе приводит в сто раз более читателей, чем суета с дизайном сайта.
Вот такие мысли.
]]>Например, вот вариант, который я написал где-то за полчаса:
-module(queens_classic). -export([solve/0]). solve() -> solve(lists:seq(1, 8), lists:seq(1, 15 + 15), 1, []). print_board([]) -> io:format("~n", []); print_board([H|T]) -> Line = [string:copies(". ", H - 1), "@ ", string:copies(". ", 8 - H)], io:format("~s~n", [Line]), print_board(T). solve(_, _, Cols, Result) when Cols > 8 -> io:format("~p~n", [Result]), print_board(Result); solve(Rows, Diags, Col, Result) -> lists:foreach(fun(Row) -> D1 = Row + Col, D2 = Row - Col + 8 + 15, T = lists:member(Row, Rows) andalso lists:member(D1, Diags) andalso lists:member(D2, Diags), if T -> Rows1 = Rows -- [Row], Diags1 = Diags -- [D1, D2], solve(Rows1, Diags1, Col + 1, [Row | Result]); true -> void end end, Rows).
Выглядит ужасно, и стиль однозначно понятно какой: C/Python на стероидах (циклы, if’ы).
А вот над этим вариантом я провозился несколько часов:
-module(queens). -export([solve/0]). solve() -> solve(1, []). print_board([]) -> io:format("~n", []); print_board([{_X, Y}|T]) -> Line = [string:copies(". ", Y - 1), "@ ", string:copies(". ", 8 - Y)], io:format("~s~n", [Line]), print_board(T). solve(X, Taken) when X > 8 -> io:format("~p~n", [Taken]), print_board(Taken); solve(X, Taken) -> L = [{X, Y} || Y <- lists:seq(1, 8), not under_attack({X, Y}, Taken)], row(L, Taken). row([], _) -> []; row([{X, Y}|L], Taken) -> solve(X + 1, [{X, Y} | Taken]), row(L, Taken). under_attack(_, []) -> false; under_attack({X, Y}, [{Xt, Yt}|L]) -> Y == Yt orelse abs(Y - Yt) == abs(X - Xt) orelse under_attack({X, Y}, L).
Вся работа со списками вручную без циклоподобных конструкций.
Печатает типа такого:
[4,7,5,2,6,1,3,8]
. . . @ . . . .
. . . . . . @ .
. . . . @ . . .
. @ . . . . . .
. . . . . @ . .
@ . . . . . . .
. . @ . . . . .
. . . . . . . @
[5,7,2,6,3,1,4,8]
. . . . @ . . .
. . . . . . @ .
. @ . . . . . .
. . . . . @ . .
. . @ . . . . .
@ . . . . . . .
. . . @ . . . .
. . . . . . . @
...
Увы, но вот эта версия мне кажется более красивой с точки зрения фукнционального стиля.
На всякий случай Makefile для обоих вариантов:
target = queens all: erlc $(target).erl erl -noshell -s $(target) solve -s init stop classic: erlc $(target)_classic.erl erl -noshell -s $(target)_classic solve -s init stop clean: -rm *.beam *.dump]]>
Погонял разные самопальные бенчмарки типа решета Эратосфена, vector<int>
vs vector<bool>
, std::string
vs char*
и т.д., пытаясь выявить улучшения или ухудшения в оптимизации. Лично я ничего кардинального не выявил по сравнению с версией 10.
Очевидно, что статический анализ кода и его безопасность в целом сейчас как никогда в моде, поэтому производители компиляторов постепенно закручивают гайки, превращая предупреждения в ошибки.
Например с ключом /sdl
Студия 11 будет считать приведенные ниже предупреждения ошибками.
Warning | Command line switch | Description |
---|---|---|
C4146 | /we4146 | A unary minus operator was applied to an unsigned type, resulting in an unsigned result |
C4308 | /we4308 | A negative integral constant converted to unsigned type, resulting in a possibly meaningless result |
C4532 | /we4532 | Use of "continue", "break" or "goto" keywords in a __finally/finally block has undefined behavior during abnormal termination |
C4533 | /we4533 | Code initializing a variable will not be executed |
C4700 | /we4700 | Use of an uninitialized local variable |
C4789 | /we4789 | Buffer overrun when specific C run-time (CRT) functions are used |
C4995 | /we4995 | Use of a function marked with pragma deprecated |
C4996 | /we4996 | Use of a function marked as deprecated |
Ссылки по теме:
Итак, для начала простой, но реальный пример - есть проект ~2000 файлов. Надо составить список используемых переменных окружения. То есть найти вхождения строк getenv(...)
и GetVariable(...)
(это наш wrapper) и выдрать из них параметр.
Задача незамысловатая и давно решается программой на C++, которая даже обход каталогов не делает, а просто вызывает юниксовый find
, генерирующий список файлов по маске, и затем по списку лопатит файлы. На 2000 файлах работает пару секунд в один поток.
Теперь Эрланг. Тут хочется замутить что-нибудь более кучерявое, чем последовательный обход файлов. MapReduce как раз в тему - можно составить список файлов, затем анализ каждого файла делать параллельно (Map), аккумулируя найденные имена переменных, и в конце обработать все полученные входждение (Reduce), в нашем случае просто подсчитать количество вхождения каждой переменной.
Фактически мой код повторяет пример из “Programming Erlang” и использует модуль phofs
(parallel higher-order functions) из этой же книги.
-module(find_variables). -export([main/0, find_variables_in_file/2, process_found_variables/3]). -define(PATH, "/Projects/interesting_project"). -define(MASK, "\\..*(cpp|c)"). main() -> io:format("Creating list of files...~n", []), % Стандартная функция обхода файловой системы. Последний параметр - % функтор, накапливающий имена в списке. Files = filelib:fold_files(?PATH, ?MASK, true, fun(N, A) -> [N | A] end, []), io:format("Found ~b file(s)~n", [length(Files)]), F1 = fun find_variables_in_file/2, % Map F2 = fun process_found_variables/3, % Reduce % Вызываем MapReduce через функцию benchmark, считающую время % выполнения. benchmark(fun() -> L = phofs:mapreduce(F1, F2, [], Files), io:format("Found ~b variable(s)~n", [length(L)]) end, "MapReduce"). benchmark(Worker, Title) -> {T, _} = timer:tc(fun() -> Worker() end), io:format("~s: ~f sec(s)~n", [Title, T/1000000]). -define(REGEXP, "(getenv|GetVariable)\s*\\(\s*\"([^\"]+)\"\s*\\)"). % Map. Анализ одного файла. find_variables_in_file(Pid, FileName) -> case file:open(FileName, [read]) of {ok, File} -> % Заранее компилируем регулярное выражение. {_, RE} = re:compile(?REGEXP), % Данный обратный вызов пошлет родительскому контролирующему % потому сообщение с именем найденной переменной. CallBack = fun(Var) -> Pid ! {Var, 1} end, find_variable_in_file(File, RE, CallBack), file:close(File); {error, Reason} -> io:format("Unable to process '~s', ~p~n", [FileName, Reason]), exit(1) end. % Reduce. Анализ данных. Данная функция вызывается контролирующим % процессом MapReduce для каждого найденного ключа вместе со списком % значений, ассоциированных с ним. В нашем случае это будут пары % {VarName, 1}. Мы просто подсчитаем для каждого VarName количество % пришедших пар, то есть количество найденных вхождений этой переменной. % Это и есть наш незамысловатый анализ. process_found_variables(Key, Vals, A) -> [{Key, length(Vals)} | A]. % Построчный обход файла. find_variable_in_file(File, RE, CallBack) -> case io:get_line(File, "") of eof -> void; Line -> scan_line_in_file(Line, RE, CallBack), find_variable_in_file(File, RE, CallBack) end. % Поиск строки в строке по регулярному выражению (скомпилированному ранее), % и в случае нахождение вызов CallBack с передачей ему имени найденной % переменной. scan_line_in_file(Line, RE, CallBack) -> case re:run(Line, RE) of {match, Captured} -> [_, _, {NameP, NameL}] = Captured, Name = string:substr(Line, NameP + 1, NameL), CallBack(Name); nomatch -> void end.
Для сборки программы нужен модуль phofs. Он является универсальным, независимым от конкретных функций Map и Reduce.
И Makefile на всякий случай:
target = find_variables all: erlc $(target).erl erlc phofs.erl erl -noshell -s $(target) main -s init stop clean: -rm *.beam *.dump
Пузомерка. Как я уже сказал, программа на C++ вместе со временем вызова find
на моей машине работает 1-2 секунды. Версия на Erlang’e работает ~20 секунд. Плохо? Смотря как посмотреть. Если анализ каждого файла будет более длительным (то есть программа будет основное время тратить на анализ файла, а не обход каталогов), то тут уже не совсем очевидно, какое из решений будет более практично при увеличении числа файлов и сложности анализа.
Я новичок в Эрланге, поэтому будут признателен за критику кода.
Посты по теме:
]]>B итоге я окончательно допилил программу, и теперь она заменила мне версию на Питоне.
Что программа умеет особенно удобного (как мне кажется):
Про Эрланг. Меня начинает реально вставлять. Я почувствовал (для многих это и не новость), что тут можно написать что-то реальное, особенно связаное с сетью и многозадачностью.
Из насущных проблем:
Пузомерка. Я сделал тест на прокач шестидесятимегового файла через питоновскую и эрланговскую версию. Результаты интересные.
Кач напрямую:
curl http://www.erlang.org/download/otp_src_R14B04.tar.gz >direct
Через Питон:
Window 1: python pyspy.py -l 50000 -a www.erlang.org -p 80 -L log
Window 2: curl http://localhost:50000/download/otp_src_R14B04.tar.gz >via-proxy-python
Через Эрланг:
Window 1: escript tcp_proxy.erl 50000 www.erlang.org 80
Window 2: curl http://localhost:50000/download/otp_src_R14B04.tar.gz >via-proxy-erlang
Файл напрямую качается, условно, минуту. Питоновская версия прокачала файл за шесть минут при включенном логе на экран и файл. Причем сброс лога и непосредственно прокач заканчивались приблизительно в одно время (данные задачи выполняются параллельно, общаясь через очередь, и технически не обязаны завершаться одновременно, так как очередь надо выгрести).
На Эрланге картина иная. Файл прокачался практически за то же время, что и напрямую! Но вот полного сброса лога я так и дождался. Через шесть минут он успел сбросить где-то 10% лога.
Выводы: Видимо, поведение питоновской версии обусловлено тем, что поток лога и потока-качалка работаются примерно с одной скоростью, поэтому в среднем очеред обмена постоянно выгребается. Фактически, скорость программы ограничена пропускной способностью потока логирования, но так как визуально не видно, что поток качания заканчиватся значительно раньше, то можно предположить, что он работается примерно с такой же скоростью (напомню, ~6 минут).
На Эрланге же качалка работает, как мне показалось, очень быстро. Данные перекачиваются и параллельно загоняются в очередь на логирование. А вот производительность логирования оставляет желать лучшего. Ради эксперимента я закомментировал вызов функции создания шестнадцатеричного дампа, и время сброса лога также упало до минуты. Поэтому, как мне кажется, корень зла в моей кривой работе со строками и списками при создании дампа (возможно что-то где-то постоянно копируются, а в мире рекурсии и изменения данных только через копирование ошибки подобного рода дорого отражаются на производильности). А вот работа с сокетами и посылкой/приемом сообщений между потоками в Эрланге очень эффективная.
Я вообще заметил, что в Эрланге ты подсознательно начинашь писать многопотоковые программы. Например, тут в принципе нет глобальных объектов. И допустим, у тебя есть флаг, глобальная установка, которую хочется иметь везде. Так как глобально ее объявить нельзя, приходится таскать как параметр функций там и сям. А как вариант “навязанного конструктивного мышления”, думаешь - а давай-ка я запущу этот кусок как поток и буду вызывать его функционал через посылку сообщений. В этом случае я могу передать мне нужный параметр один раз в начале при создании потока, тем самым сделав его типа глобальным для этого потока.
Наверное пример вышел немного скомканным, но общая идея такова - так как ты обязан передавать в функцию все ее параметры каждый раз, то начинаешь думать об максимальной независимости и дроблении функционала, что как следствие, позволяет запускать их в разных потоках.
Для интересующихся - исходник доступен.
Посты по теме:
]]>После десятков версий изощренных скриптов я решил собрать волю в кулак и начать использовать Git.
Задача: на виндовой машине (это мой основной рабочий ноутбук) установить Git как сервер, чтобы я мог всегда иметь на нем самую актуальную копию всего. Затем установить Git как клиент на рабочих машинах и обмениваться commit’ами через центральный репозиторий на Windows.
Для простоты я решил использовать SSH как протокол. Благо все UNIX машины имеют ssh-клиент.
Плюсы - везде Git, все локальные изменения имеют версии и можно вести локальные ветки. Ну и центральная копия - тоже под Git. Минусы - потратить время и все это настроить.
Git/ssh как сервер на Windows - это целая тема, так как нужно поставить SSH сервер и правильно прикрутить к нему Git.
Благодаря двум (1, 2) ссылкам удалось настроить CopSSH в паре с msysgit.
Далее Git на клиентских машинах. С Linux и Windows все совсем просто (на Windows используется тот же msysgit).
На Solaris пришлось собрать GNU Make до 3.82 (на 3.75 Git не собирается).
На HPUX and AIX пришлось собрать coreutils (для нормального install), less (представляете, на HPUX нету less по умолчанию), python (опять для HPUX), zlib и свежие tcl/tk.
Один день на борьбу c CopSSH на Windows и день на сборки под UNIXами.
Зато теперь радость и благодать.
P.S. С CopSSH интересная история. Вчера (21 ноября) на их сайте можно было все скачать. Сегодня (22 ноября) читаю на том же сайте:
Termination of free solutions from ITeF!x
Submitted by tk on Tue, 22⁄11/2011 - 08:18 itefix
Development,distribution and support of free solutions from Itefix are now terminated due to lack of time and changes in priorities.
С их зеркала на sourceforge.net также пропали все файлы. Хорошо, что я не удалил дистрибутив CopSSH, скачанный вчера.
Ссылки по теме:
]]>Программа по функциям идентична версии на Питоне за исключением отсутствия дублирования лога в файл и продвинутого разбора флагов командной строки.
И так: программа многопоточна, и журналирование также происходит в отдельном потоке для обеспечения целостности многострочных дампов.
Про Эрланг. После многократных и пока полностью неуспешных заходов на Хаскелл и после все еще неудачных попыток на Lisp или Scheme написать что-то более менее реальное и жизненное, Эрланг был реальным прорывом для меня.
Удивительно, невозможность изменять переменные (представьте, что программируя на С++ надо все переменные делать const
) является фантастическим способом борьбы с опечатками при cut-and-paste. Также когда делаешь циклы через хвостовую рекурсию, сразу осознаешь, как эффективно работать со списками, чтобы их не копировать, а всегда “таскать” за хвост или голову.
Ну а концепция легких потоков и обмена сообщениями между ними (как в Go), приправленная глобальной иммутабельностью, позволяет легко писать надежные многотопочные программы.
Например, истересен способ реализации многопотокового TCP/IP сервера. Обычно при его программировании есть распростраенный прием: один главный поток, принимающий соединения, и когда соединение принято, создается новый поток-исполнитель, который обрабатывает соединение и после этого умирает.
В Эрланге можно сделать иначе (функция acceptor()
). Поток, ожидающий входящего соединения, после его получения рождает свой клон для ожидания следующего соединения и затем сам обрабабатывает запрос.
Для меня это было немного необычно.
-module(tcp_proxy). -define(WIDTH, 16). main([ListenPort, RemoteHost, RemotePort]) -> ListenPortN = list_to_integer(ListenPort), start(ListenPortN, RemoteHost, list_to_integer(RemotePort)); main(_) -> usage(). usage() -> io:format("~ntcp_proxy.erl local_port remote_port remote_host~n~n", []), io:format("Example:~n~n", []), io:format("tcp_proxy.erl 50000 google.com 80~n~n", []). start(ListenPort, CalleeHost, CalleePort) -> io:format("Start listening on port ~p and forwarding data to ~s:~p~n", [ListenPort, CalleeHost, CalleePort]), {ok, ListenSocket} = gen_tcp:listen(ListenPort, [binary, {packet, 0}, {reuseaddr, true}, {active, true}]), io:format("Listener socket is started ~s~n", [socket_info(ListenSocket)]), spawn(fun() -> acceptor(ListenSocket, CalleeHost, CalleePort, 0) end), register(logger, spawn(fun() -> logger() end)), wait(). % Infinine loop to make sure that the main thread doesn't exit. wait() -> receive _ -> true end, wait(). format_socket_info(Info) -> {ok, {\{A, B, C, D}, Port}} = Info, lists:flatten(io_lib:format("~p.~p.~p.~p:~p", [A, B, C, D, Port])). peer_info(Socket) -> format_socket_info(inet:peername(Socket)). socket_info(Socket) -> format_socket_info(inet:sockname(Socket)). acceptor(ListenSocket, RemoteHost, RemotePort, ConnN) -> case gen_tcp:accept(ListenSocket) of {ok, LocalSocket} -> spawn(fun() -> acceptor(ListenSocket, RemoteHost, RemotePort, ConnN + 1) end), LocalInfo = peer_info(LocalSocket), logger ! {message, "~4.16.0B: Incoming connection from ~s~n", [ConnN, LocalInfo]}, case gen_tcp:connect(RemoteHost, RemotePort, [binary, {packet, 0}]) of {ok, RemoteSocket} -> RemoteInfo = peer_info(RemoteSocket), logger ! {message, "~4.16.0B: Connected to ~s~n", [ConnN, RemoteInfo]}, exchange_data(LocalSocket, RemoteSocket, LocalInfo, RemoteInfo, ConnN, 0), logger ! {message, "~4.16.0B: Finished~n", [ConnN]}; {error, Reason} -> logger ! {message, "~4.16.0B: Unable to connect to ~s:~s (error: ~p)~n", [ConnN, RemoteHost, RemotePort, Reason]} end; {error, Reason} -> logger ! {message, "Socket accept error '~w'~n", [Reason]} end. exchange_data(LocalSocket, RemoteSocket, LocalInfo, RemoteInfo, ConnN, PacketN) -> receive {tcp, RemoteSocket, Bin} -> logger ! {received, ConnN, Bin, RemoteInfo, PacketN}, gen_tcp:send(LocalSocket, Bin), logger ! {sent, ConnN, LocalInfo, PacketN}, exchange_data(LocalSocket, RemoteSocket, LocalInfo, RemoteInfo, ConnN, PacketN + 1); {tcp, LocalSocket, Bin} -> logger ! {received, ConnN, Bin, LocalInfo, PacketN}, gen_tcp:send(RemoteSocket, Bin), logger ! {sent, ConnN, RemoteInfo, PacketN}, exchange_data(LocalSocket, RemoteSocket, LocalInfo, RemoteInfo, ConnN, PacketN + 1); {tcp_closed, RemoteSocket} -> logger ! {message, "~4.16.0B: Disconnected from ~s~n", [ConnN, RemoteInfo]}; {tcp_closed, LocalSocket} -> logger ! {message, "~4.16.0B: Disconnected from ~s~n", [ConnN, LocalInfo]} end. logger() -> receive {received, Pid, Msg, From, PacketN} -> io:format("~4.16.0B: Received (#~p) ~p byte(s) from ~s~n", [Pid, PacketN, byte_size(Msg), From]), dump_bin(Pid, Msg), logger(); {sent, Pid, ToSocket, PacketN} -> io:format("~4.16.0B: Sent (#~p) to ~s~n", [Pid, PacketN, ToSocket]), logger(); {message, Format, Args} -> io:format(Format, Args), logger() end. dump_list(Prefix, L, Offset) -> {H, T} = lists:split(lists:min([?WIDTH, length(L)]), L), io:format("~4.16.0B: ", [Prefix]), io:format("~4.16.0B: ", [Offset]), io:format("~-*s| ", [?WIDTH * 3, dump_numbers(H)]), io:format("~-*s", [?WIDTH, dump_chars(H)]), io:format("~n", []), if length(T) > 0 -> dump_list(Prefix, T, Offset + 16); true -> [] end. dump_numbers(L) when (is_list(L)) -> lists:flatten([io_lib:format("~2.16.0B ", [X]) || X <- L]). dump_chars(L) -> lists:map(fun(X) -> if X >= 32 andalso X < 128 -> X; true -> $. end end, L). dump_bin(Prefix, Bin) -> dump_list(Prefix, binary_to_list(Bin), 0).
В работе может выводить примерно следующее:
alexander:erlang/>./tcp_proxy.sh 50000 pop.yandex.ru 110
Start listening on port 50000 and forwarding data to pop.yandex.ru:110
Listener socket is started 0.0.0.0:50000
0000: Incoming connection from 127.0.0.1:51402
0000: Connected to 213.180.204.37:110
0000: Received (#0) 38 byte(s) from 213.180.204.37:110
0000: 0000: 2B 4F 4B 20 50 4F 50 20 59 61 21 20 76 31 2E 30 | +OK POP Ya! v1.0
0000: 0010: 2E 30 6E 61 40 32 35 20 67 55 62 44 54 51 64 5A | .0na@25 gUbDTQdZ
0000: 0020: 6D 6D 49 31 0D 0A | mmI1..
0000: Sent (#0) to 127.0.0.1:51402
0000: Received (#1) 11 byte(s) from 127.0.0.1:51402
0000: 0000: 55 53 45 52 20 74 65 73 74 0D 0A | USER test..
0000: Sent (#1) to 213.180.204.37:110
0000: Received (#2) 23 byte(s) from 213.180.204.37:110
0000: 0000: 2B 4F 4B 20 70 61 73 73 77 6F 72 64 2C 20 70 6C | +OK password, pl
0000: 0010: 65 61 73 65 2E 0D 0A | ease...
0000: Sent (#2) to 127.0.0.1:51402
0000: Received (#3) 11 byte(s) from 127.0.0.1:51402
0000: 0000: 50 41 53 53 20 70 61 73 73 0D 0A | PASS pass..
0000: Sent (#3) to 213.180.204.37:110
0000: Received (#4) 72 byte(s) from 213.180.204.37:110
0000: 0000: 2D 45 52 52 20 5B 41 55 54 48 5D 20 6C 6F 67 69 | -ERR [AUTH] logi
0000: 0010: 6E 20 66 61 69 6C 75 72 65 20 6F 72 20 50 4F 50 | n failure or POP
0000: 0020: 33 20 64 69 73 61 62 6C 65 64 2C 20 74 72 79 20 | 3 disabled, try
0000: 0030: 6C 61 74 65 72 2E 20 73 63 3D 67 55 62 44 54 51 | later. sc=gUbDTQ
0000: 0040: 64 5A 6D 6D 49 31 0D 0A | dZmmI1..
0000: Sent (#4) to 127.0.0.1:51402
0000: Disconnected from 213.180.204.37:110
0000: Finished
0001: Incoming connection from 127.0.0.1:51405
0001: Connected to 213.180.204.37:110
0001: Received (#0) 38 byte(s) from 213.180.204.37:110
0001: 0000: 2B 4F 4B 20 50 4F 50 20 59 61 21 20 76 31 2E 30 | +OK POP Ya! v1.0
0001: 0010: 2E 30 6E 61 40 33 30 20 70 55 62 41 72 52 33 74 | .0na@30 pUbArR3t
0001: 0020: 6A 65 41 31 0D 0A | jeA1..
0001: Sent (#0) to 127.0.0.1:51405
0001: Received (#1) 6 byte(s) from 127.0.0.1:51405
0001: 0000: 51 55 49 54 0D 0A | QUIT..
0001: Sent (#1) to 213.180.204.37:110
0001: Received (#2) 20 byte(s) from 213.180.204.37:110
0001: 0000: 2B 4F 4B 20 73 68 75 74 74 69 6E 67 20 64 6F 77 | +OK shutting dow
0001: 0010: 6E 2E 0D 0A | n...
0001: Sent (#2) to 127.0.0.1:51405
0001: Disconnected from 213.180.204.37:110
0001: Finished
Вывод: Эрланг - прекрасный вариант для начала функциональной карьеры.
Посты по теме:
]]>У меня такой задачей стал многопотоковый перехватчик-прокси для TCP/IP. Такая программа ставится как промежуточное звено между клиентом и сервером, и можно удобно смотреть, что летает в канале туда-сюда.
Данная задача затрагивает многие аспекты языка - потоки, синхронизацию, сокеты, ввод/вывод, работу со строками.
Вот, например, версия на Питоне. На данный момент это наиболее удачная моя версия, так как она работает на многих платформах благодаря Питону и также показывает неплохую производительность благодаря логгированию в параллельном потоке.
Еще я писал это программу на C, C++, C++/boost, PHP, VB, Go, Ruby. На Erlang’e не осилил, пока.
А вы чем тестируете новые языки?
]]>С putenv()
у меня уже был интересный опыт.
Часто люди пишут так:
if (getenv("A_FLAG")) { ... }
Это работает неплохо для переменных-флагов, которые либо есть, либо нет. Значение не важно.
Что получилось у меня:
int main(...) { putenv("GLOBAL_FLAG=1"); // Глобальное значение для всей программы. ... system("xyz"); // Это программа должна видеть GLOBAL_FLAG=1. ... do_stuff(); ... } void do_stuff() { ... if (something) { putenv("GLOBAL_FLAG="); // Убрать переменную. system("abc"); // А вот для этой программы флаг должен быть убран. ... } ... if (getenv("GLOBAL_FLAG") { // И вот тут начиналась ерунда на разных платформах. } }
А корень зла тут в том, что после putenv()
результат getenv()
может стать либо NULL
, либо ""
, в зависимости от платформы.
Например:
if (getenv("GLOBAL_FLAG") { ...
работает только на Windows и правильнее писать:
const char* p = getenv("GLOBAL_FLAG"); if (p != NULL && *p != '\0') { ... }
И лучше сделать wrapper для getenv()
:
std::string GetEnv(const char* name) { const char* v = getenv(name); return v ? v : ""; }
И для проверки писать:
if (!GetEnv("var").empty()) { .. }
Для теста я написал программу, которая выставляет переменную и проверяет ее значение через getenv()
и через вызов дочерней программы.
#include <string> #include <vector> #include <iostream> #include <cstdlib> #ifdef WINDOWS #define putenv _putenv #endif void PrintVariableViaShell(const std::string& name) { std::cout << "Value from shell:" << std::endl; const std::string cmd = #ifdef WINDOWS std::string("cmd /c echo [%") + name + "%]"; #else std::string("/bin/sh -c \"echo [$") + name + "]\""; #endif std::cout << cmd << std::endl; std::system(cmd.c_str()); } void PrintVariableViaGetEnv(const std::string& name) { std::cout << "Value from getenv():" << std::endl; const char* v = std::getenv(name.c_str()); std::cout << "[" << (v ? v : "<NULL>") << "]" << std::endl; } void SetVariableDeleteAndPrint(const char* name_value, const bool equ) { const std::string& name_value_s(name_value); const std::string name = name_value_s.substr(0, name_value_s.find('=')); putenv(const_cast<char*>(name_value)); std::vector<char> delete_without_equ(name.begin(), name.end()); delete_without_equ.push_back('\0'); putenv(&delete_without_equ[0]); std::cout << "Value after deleting WITHOUT '=':" << std::endl; PrintVariableViaShell(name); PrintVariableViaGetEnv(name); std::cout << std::endl; putenv(const_cast<char*>(name_value)); std::vector<char> delete_with_equ(name.begin(), name.end()); delete_with_equ.push_back('='); delete_with_equ.push_back('\0'); putenv(&delete_with_equ[0]); std::cout << "Value after deleting WITH '=': " << std::endl; PrintVariableViaShell(name); PrintVariableViaGetEnv(name); } int main(int argc, char* argv[]) { #ifdef WINDOWS std::cout << "WINDOWS" << std::endl; #else system("uname"); #endif SetVariableDeleteAndPrint("ABC=123", true); return 0; }
И вот результы с разных платформ.
Linux
Linux
Value after deleting WITHOUT '=':
Value from shell:
/bin/sh -c "echo [$ABC]"
[]
Value from getenv():
[<NULL>]
Value after deleting WITH '=':
Value from shell:
/bin/sh -c "echo [$ABC]"
[]
Value from getenv():
[]
AIX
AIX
Value after deleting WITHOUT '=':
Value from shell:
/bin/sh -c "echo [$ABC]"
[123]
Value from getenv():
[123]
Value after deleting WITH '=':
Value from shell:
/bin/sh -c "echo [$ABC]"
[]
Value from getenv():
[]
SunOS
SunOS
Value after deleting WITHOUT '=':
Value from shell:
/bin/sh -c "echo [$ABC]"
[123]
Value from getenv():
[123]
Value after deleting WITH '=':
Value from shell:
/bin/sh -c "echo [$ABC]"
[]
Value from getenv():
[]
HP-UX
HP-UX
Value after deleting WITHOUT '=':
Value from shell:
/bin/sh -c "echo [$ABC]"
[123]
Value from getenv():
[123]
Value after deleting WITH '=':
Value from shell:
/bin/sh -c "echo [$ABC]"
[]
Value from getenv():
[]
WINDOWS
WINDOWS
Value after deleting WITHOUT '=':
Value from shell:
cmd /c echo [%ABC%]
[123]
Value from getenv():
[123]
Value after deleting WITH '=':
Value from shell:
cmd /c echo [%ABC%]
[%ABC%]
Value from getenv():
[<NULL>]
Только на Windows getenv()
возвращает NULL
после удаления. На остальных это будет пустая строка.
Забавно, на Linux можно удалять переменные через putenv("name")
(без знака “=”), а тогда getenv()
будет возвращать NULL
.
Посты по теме:
]]>Понятно, что все зависит от типа работы, поэтому даю конкретный пример.
Дано: работа программиста над устоявшимся продуктом, который имеет major релизы раз в год; в течение года выпускаются minor релизы и критические исправления; плюс в поддержке находятся релизы последних 5-6 лет.
Вот мой расчет:
365 дней в году 365 / 7 = 52 недели в году 52 * 5 = 260 рабочих дней в году 260 - 25 = 235 рабочих дней в году за вычетом отпуска 235 - 10 = 225 рабочих дней в году за вычетом еще и государственных выходных 225 - 15 = 210 еще минус две недели в среднем “по болезни”
Итак: 210 человеко-восьмичасовых-дней.
Общение с несколькими менеджерами программистов показало, что цифра “100 дней” является весьма распространенной как оценка сверху на то, сколько новой разработки планировать в год на человека.
Получается, что после поддержки, затрат на внутренюю инфраструктуру, совещаний и обучений реально на разработку остается только половина времени.
]]>Ребята купили в офис “Test Driven Development for Embedded C” от James W. Grenning.
Хоть там и много про hardware, но примеры как можно и нужно изолировать зависимости для упрощения тестирования на языке С очень полезные. Кроме того описаны пара xUnit библиотек для этого языка (хотя cmockery нет).
Итак, цитата (как есть, без сокращении и перевода).
Commented-out Code
Sources files littered with commented-out code are an ugly mess. New or returning programmers are faced with questions about what the code is supposed to do. “Should the code be uncommented?” “It is no longer needed?” “When will it be needed and under what circumstances is it needed?” “What’s that for?”
The solution to this code smell is simple; delete the commented-out code. It can always be recovered from your source repository.
James W. Grenning
Посты по теме:
]]>new
, now
и old
являются запрещенными для употребления в комментариях, именах переменных окружения, описаниях коммитов, когда речь идет об изменении поведения чего-либо.
Например, переменная окружения, включающая “старый” алгоритм генерации ключа: вместо OLD_KEY_ALGORITHM
должно быть USE_ABCDEF_ALGORITHM
.
Вместо ссылок на прошлое или будущее надо описывать конкретное поведение, так как характеристики new
, now
and old
становятся неактуальными уже через неделю. А еще хуже, что они приводят к появлению таких имен как OLD_2_KEY_ALGORITHM
или NEW_2_KEY_ALGORITHM
.
const
: использовать const
абсолютно везде, где это не приводит к ошибкам компиляции.
Я не имею ввиду очевидные применения const
для аргументов функций и константных методов.
Я имею ввиду, что любая переменная, которая не меняется с момента ее создания, должна быть const
. Само название “переменная” значит, что объект должен меняться. Если он не меняется, то это уже не переменная. Сделать переменную непемеренной в C/C++ можно с помощью ключевого слова const
.
Пара примеров, где люди часто не ставят const
:
const int sz = very_long_name_to_calculate_size(...); for (int i = 0; i < sz; ++i) { ... const VeryLongClassName var(a, lot, of, different, parameters); int a = var.getX(); // Only "const" functions of "VeryLongClassName" are called further down.
Кстати, явное указание на неизменность переменной помогает компилятору в оптимизации.
Посты по теме:
]]>Поехал посмотреть и магазин и Хромбук.
“Магазин” - это стойка из двух столов в десятком демо ноубутов в зале огромного PC World‘а. Рядом отдел Apple, Sony и т.д. То есть это не целый отдельный магазин, как у Apple, например.
Хромбук, который там демонстрируется и продается - Samsung Series 5 3G.
Ниже будут не очень хорошего качества фотографии моего часового знакомства с данным устройством.
В закрытом виде.
Слева - питание, странного вида видео-разъем, USB и аудио.
Справа - еще один USB.
Спереди - SD.
Разъем питания в целом хлюпковат.
Посмотрим клавиатуру.
Над шифтом есть интересная кнопка Search, которая просто открывает в браузере новую пустую закладку и устанавливает курсор в строку поиска. По сути это CTRL-T. Мне эта кнопка очень понравилась.
В верхнем ряду кнопки для интернета. Та, что между полноэкранным режимом и яркостью - переключение между окнами Хрома, если их открыто несколько.
Кнопок Ins, Del, Page Up и Page Down нет.
После включения через 8 секунд появляется приглашение логина и пароля от вашего гугловского аккаунта (я воспользовался демо-логином), и затем появляется главный экран.
Все, что есть в Хромбуке - это браузер Хром. Войти/выйти из него нельзя. Можно только открывать новые табы и окна.
Сам Хром отличается от обычной десктопной версии разными дополнительными меню. Например, сетевые найстройки.
WiFi
Русский язык можно также добавить в настройках.
Печатать можно либо подключив USB-принтер (есть спискок поддерживаемых моделей), либо “правильным” гугловым методом через Cloud Print.
В Хромбуке можно хранить локальные файлы. Их можно приаттачивать к письмам, например.
В самом локальном хранилище реально можно что-то делать только с файлами определенного типа, например, фотографиями или видео. Их можно закачивать на WebPicasa.
В локальное хранилище попадают все скачиваемые файлы и скриншоты.
Скайпа тут быть не может, поэтому я проверил GTalk (в окне моя бритая голова).
Настройки GTalk.
После небольшего ерзанья GTalk все же упал.
Конечно я зашел на правильный сайт.
И еще на один.
И еще на один для оценки производительности.
Линукс загрузился за 33 секунды. На моем Mac Air Core Due оно грузится за 8 секунд. А вот богомипсы почему-то такие же - ~20.
Теперь мой субъективный вывод.
Стоит эта музыка 400 фунтов (~630 долларов), что, как мне кажется, является запредельно нереальной ценой. Сам ноут крайне прост и не является супер легким и тонким ультрабуком, за который можно было бы драть такую цену.
Рядом лежали десятки нетбуков прочих фирм за похожие и меньшие деньги, на которых можно делать все тоже самое, плюс все остальное.
Даже в магазине, где был четкий вайфай, работать с полным онлайном все же напряжно, так как сетевые тормоза там сям начинают очень быстро нервировать. Стандартный офлайновый Gmail там есть, но работать в нем реально нельзя.
Если цена была бы 50 фунтов - лично я купил бы его прямо там, так как использую продукты Google и рад поиграться с подобной “консолью от Гугл”, но за 400 фунтов - как-нибудь обойдусь.
]]>Увы, но наследие 32-х бит сильно мешает и засоряет код. И одна их ключевых проблем - базовые типы.
Сейчас мы пришли к выводу, что зоопарк типов надо прекращать.
Итак, правило.
Слова char
, short
, long
, long long
, __int64
по умолчанию запрещены. Их использование придется объяснить на code review. Если нужна переменная, которая логически не является смещением в памяти, и ее предельные значения с запасом укладываются в интервал от -2^31 до 2^31-1, по это просто int
. Если это индекс или смещение в памяти - это size_t
.
Если же нужен беззнаковый тип, или конкретный размер, то надо использовать типа из stdint.h
(int8_t
, uint8_t
, uint16_t
, int16_t
, uint32_t
, int64_t
, uint64_t
), которые аккуратно реализованы для каждой платформы в одном единственном месте.
Шаг радикальный, посмотрим как пойдет.
Кстати, как вы справляетесь вот с этим?
for (int i = 0; i < s.length(); ++i)
Убрать предупреждение о приведении size_t
к int
можно, изменив на:
for (size_t i = 0; i < s.length(); ++i)
Но как быть с:
for (int i = s.length(); i >= 0; --i)
Тут уже на size_t не изменить и надо внучную писать int i = static_cast<int>(s.length())
, что, конечно, плохо.
Похожая петрушка с strlen()
, также возвращающей size_t
.
Рассмотрим некоторые.
Windows: telnet.exe
Этой программой можно пользоваться только случайно, по несколько минут, когда ничего другого нет под рукой, а надо быстренько зайти. Если надо именно ssh, а не telnet - ставим SecureCRT или на худой конец putty. Проблема не в telnet.exe как таковой, а в реализации консоли.
Mac: благодаря нормальной консоли (табы, полноэкранный режим, масштабирование) можно просто пользоваться штатным telnet и ssh.
Windows: штатный просмотр картинок - голимая программа. Реально хочется сразу поставить XnView или ACDSee.
Mac: Preview в сочетании с Finder (см. ниже) - удобнейшая вещь, понимающая не только картинки, но и PDF, текстовики и т.д. Для картинок есть встроенный редактор для минимального редактирования. В Windows надо открывать Paint.
Windows: Можно сделать снимок либо окна, либо всего экрана. Снимок ложится в буфер обмена, откуда его надо куда-то перенести. Хочется сразу поставить, например, FastStone Capture.
Mac: встроенная система снятия снимков экрана умеет все. Можно даже делать это из командной строки.
Windows: IE - без комментариев. Нужен FF/Chrome/Opera.
Mac: Safari является одним из топовых браузеров. Хотя Chrome на Maке все еще актуален из-за синхронизации с Google, Flash и расширений.
Windows: Windows Mail (бывший Outlook Express) вполне можно использовать от лени, но Thunderbird лучше поставить.
Mac: Apple Mac (особенно 5.0 в Lion) даст фору Thunderbird’у и даже Microsoft Outlook’у (хотя надо отдать должное - Microsoft Outlook 2011 for Mac чертовски хорош).
Windows: cmd.exe - без комментариев. Это не скриптование, а ёрзанье. cscript может еще как-то спасти ситуацию.
Mac: ksh/bash/csh/python/php/ruby - на выбор из коробки.
Windows: встроенных программ для этого нет. Может через cscript можно?!
Mac: AppleScript и Automator позволяют делать почти все. Уж как минимум кнопочки на окнах нажимать можно.
Backup
Windows: вообще есть люди, реально использующие встроенную в Windows систему архивирования?
Mac: Time Machine является удобнейшей системой инкрементального бэкапа. Можно в любой папке “отмотать” время назад и вернуть файлы к нужной дате. Сохранения могут делаться локально, без внешнего накопителя, если место есть на диске, а когда накопитель появится в доступности (по USB или сети), то сохранения автоматически уходят туда.
Например, когда ноут у меня дома, то по Wifi он всегда видит NAS, и делает сохранения туда, периодически. Если у меня много изменений, то я могу подключиться кабелем, чтобы слилось быстрее. А когда ноут не дома, все накапливается локально, и по приходу домой происходит сливание. Да, заметьте, все описанное происходит автоматически, в фоне.
Словарь
Windows: нет
Mac: Приложение Dictionary интегрировано в систему и вызыватся кликом на слове или фразе почти отовсюду. Для русского языка есть словари от Лингво, правда неофициальные.
Запуск программ и документов из командной строки
Windows: В XP я использовал TypeAndRun. В Vista и 7 уже неактуально, так как в Start > Run можно набирать имя программы, и будут выводиться варианты программ и документов, доступных в системных путях. Жить можно.
Mac: Увы, но Spotlight (вызываемый по Сmd+Space) - это глобальный индекс по всему, что есть на файловой системе, обновление которого происходит каждый раз при ее изменении. Какие-либо логические “пути” тут не при чем.
Работа с файлами
Windows: Explore (Проводник). В целом с этой программой можно жить. Но лично моя программистская натура требует двух панелей в том или ином виде. Обычно это кончается FAR’ом или Total Commander’ом.
Mac: У меня есть двухпанельный менеджер файлов на Маке, muCommander. До Фара или TC, конечно, как до луны, но жить можно. Но! Поработав в Finder (“Проводник” на Маке) некоторое время, я начал замечать, что запускаю muCommander все реже и реже. Не могу точно описать почему, просто не нужно. Возможно просто из-за общей продуманности интерфейса и возможностей Finder’а.
Пока все, что волнует лично меня.
Как-то не понятно - почему Windows комплектуется столь недоразвитыми штатными средствами? Увы, часто понять это можно только поработав там, где программы “из коробки” нормальные.
P.S. Не хочу развязывать религиозные споры, но замечания с реальными проблемами и методами их решения, конечно, приветствуются.
]]>void* p = (struct this_does_not_exist *) -1;
Убираем struct
для С++ и получаем:
cast.cpp
cast.cpp(1) : error C2065: this_does_not_exist: необъявленный идентификатор
cast.cpp(1) : error C2059: синтаксическая ошибка: )
Теперь добавляем forward declaration:
class this_does_not_exist; void* p = (this_does_not_exist *) -1;
Тут все честно и компилируется без ошибок.
Скорое всего тут везде все по стандарту, но первый пример все-таки какой-то странный.
GCC хотя бы предупреждение выдает.
]]>Сложные, связанные с использованием почти всех ресурсов вычисления происходят в kernel space – читай: когда нужно максимально точное понимание действительности, или надо изложить запутанную мысль в каскаде таких же запутанных мыслей, то это однозначно происходит на родном языке, и только в самом конце результат уходит на вывод (читай: перевод на язык среды) в user space.
Простые, бытовые вещи могут обрабатываться без перевода (на неродном языке) и по мере увеличения знаний и опыта в нем, сложность возможной обработки в user space может увеличиваться, тем самым разгружая процессор.
Есть также люди, имеющие несколько родных языков. У таких людей просто многоядерный или многопотоковый процессор и более сложное ядро, умеющее распределять задачи между двумя или более языковыми потоками (читай: языками).
Можно пойти немного дальше.
Если человек живет и работает в родной языковой среде – он работает в командном процессоре, когда входная и выходная обработка данных весьма простая. В неродной среде – это работа в сложном UI, когда данные проходят длинный путь окон, локализации и разнообразных преобразований, чтобы поспасть на обработку в kernel space и в конце быть доставленными обратно на экран (читай: во внеший мир).
]]>rm
, установок и сносов софта или игр с ядром, когда проще всю систему просто переставить, постепенно приходит понимание, что консоль рута чревата последствиями при неаккуратной работе.
А при работе с боевыми серверами, когда можно одной неверной командой остановить работу многих людей, может лучше поработать в паре, чтобы случайно не грохнуть чего.
Как говорил один мой знакомый администратор, матерый ораклоид и юниксист: “Я никогда лишний раз в root’ом не зайду, а то вдруг чего надует (ветром)”.
Тут как в армии - переходишь в режим рута - встаешь смирно, одной рукой отдаешь честь (сам себе), а второй аккуратно делаешь работу. И как только все сделано, закрываешь рутовую консоль.
А если серьезно, то бывает и так: работаешь в терминале, правишь настройки машины, собираешься уже было сказать reboot, когда понимаешь, что это консоль боевого сервера, а не девелоперской машины.
Лично я все консольные сессии к важным серверам делаю с максимально неудобной цветовой гаммой, например, зеленое на красном фоне, чтобы вот так не ошибаться.
Если разобраться, в юниксе, если ты не рут, то обычно крайне сложно сделать что-то, ломающее систему, хотя работать это не мешает. И это классное чувство. Пока ты явно не сказал su
или sudo
, ты на 99% спокоен, так как изменить что-то за пределами домашнего каталога система не даст.
Увы, в Windows нереально работать, не будучи формально администратором, хотя в Windows 7 ситуация улучшилась. Когда я пересел с Windows XP на семерку, я стал регулярно отвечать на вопрос о том, что какая-то программа хочет сделать что-то с правами администратора (хотя мой пользователь есть администратор в понятии Windows), и лично мне это нравится. Есть какое-то минимальное ощущения контроля на тем, что кто-то больше не “размазывает” DLL’ки по системе без моего ведома, от которых ей скоро поплохеет.
]]>Я опишу, как сам организую файлы, в форме почти разрозненных идей-заметок. Может что-то будет полезно кому-то, и может кто-то поделится своими приемами.
На Мак я пересел недавно, и рабочий ноутбук у меня все равно на Windows 7, то большинство приемов ориентированы на Windows.
Основная цель мой структуры - минимизировать и максимально упростить бэкап и потенциальную миграцию на другой компьютер. При правильном подходе эти задачи можно решать как одну.
Все рекомендации ниже движимы именно этой целью.
Один из подходов для решения обоих задач - сделать так, чтобы все мои файлы были доступны в иерархии одного единственного каталога. “Мои” означает, что в случае переезда на другой компьютер, данные файлы должны быть перенесены (не забыты). У меня есть каталог c:\moe
(этимология английского “слова” moe
утеряна). Двигаясь по этому каталогу вниз, можно найти все “мои” файлы, все-все. Также важно, что в каталоге c:\moe
нет мусора, которым обычно загажен My Documents
(всякие проекты-примеры, создаваемые Студией без спроса, мириады прочих папок My...
от разных программ и т.д.). То есть это чистая полезная информация без неконтролируемого мусора.
К чему такие сложности? Не секрет, что в Windows расположение некоторых файлов “по умолчанию” весьма необычно, и далеко не все программы позволяют менять их расположение. Попробуйте найти базу данных Outlook Express или Microsoft Outlook, профайлы Thunderbird’а, Chrome’а или iTunes, настройки сессий SecureCRT и т.д. Все они обычно скрываются где-то в недрах c:\Users\your_user_name
. Можно тупо бэкапить этот каталог, но, опять, там уж очень много неконтролируемого г*вна.
Итак, к помощью символьных ссылок я привел все нужные каталоги к простым путям. Например:
c:\moe\db\
outlook\..
thunderbird\..
profiles\
chrome\..
securecrt\..
itunes\..
skype\..
Идем далее. Последнее время я стал люто ненавидеть программы, которые сохраняют конфигурацию и настройки в реестре. Я еще могу понять, что Microsoft Office пользуется реестром из-за “глубокой интеграции” в систему. Но вот почему Far или putty до сих пор не могут просто пользоваться текстовым файлом для настроек и сохранять его в c:\Users\...
- непонятно. Для таких программ у меня написан скрипт, который командой reg копирует настройки нужных веток реестра в текстовые файлы. Что такого полезного есть в настройках Far? Например, ftp/sftp сессии. У меня их около сотни, и потерять их я не хочу.
Отдельной строкой хочу отметить программы, которыми можно использоваться в portable режиме. У меня есть отдельный каталог для них, например:
c:\moe\live\
7z-9.20\..
hiew-8.00\..
winrar-3.70\..
xnview-1.98.2\..
putty-0.60\..
notepad++-5.9.2\..
...
Обычно в этом каталоге я держу небольшие программы, которые можно просто копировать без установки, и без которых некомфортно. А так - просто перенес это каталог на другую машину, и минимальный набор утилит уже есть.
Далее, каталог c:\moe\cmd
. Тут лежат разные самодельные скрипты и однофайловые микро утилиты типа junction.exe. Этот каталог находится под контролем git, так как скрипты часто меняются, и хочется видеть историю изменений.
Каталог c:\moe\projects
.
Тут лежит все, связанное с программированием. Иерархия этого каталога обычно одноуровневая: проект -> каталог. Одно условие (подтвержденное годами) - не использовать пробелы в именах каталогов. Сильно упрощает скриптование.
Например, c:\moe\projects\easy-coding\
.
Также полезно сделать линк каталога c:\Program Files\
в c:\prg
. Проще работать в скриптах с программами, установленными в этом каталоге.
Еще один каталог - c:\moe\documents
.
Тут лежат то, что называется документами, без привязки к какому-то проекту: doc(x), xls(x), pdf, jpg и т.д. Тут уже допустимы имена с пробелами и русским буквами. Внутренняя иерархия - по смыслу. С документами на работе я часто создают подкаталоги в номером года. В них удобно убирать старье. Например:
c:\moe\documents\Team meetings\
2010\... <- Прошлый год
... <- Нынешняя текучка
Некоторое время назад я начал активно пользоваться атрибутом read-only. Если я вижу, что не меняю файл некоторое время, например, пару месяцев, то я делаю его read-only. Это относится и к проектам и к документам. Атрибут read-only позволяет исключить случайную перезапись старого файла, открытого для получения “примера” из прошлого. Ну а уж если захочется его таки изменить, то надо будет снимать флаг read-only или делать копию через “Save as…”. Основная идея - старый файл не надо перезаписывать, так как история его создания может быть подзабыта, и можно случайно похерить важный файл. Все его производные должны быть сделаны через клонирование.
Кстати, в Mac OS Lion компания Apple сделала функцию “залочки” файлов, которые не менялись некоторое время.
Documents will also automatically be locked if they’re not modified for a little while. The auto-lock time is configurable in the “Options…” screen of the Time Machine preference pane (of all places), with values from one day to one year. The default is two weeks.
Есть еще несколько каталогов:
c:\moe\books
- книгиc:\moe\itunes
- база данных iTunesc:\moe\soft
- архив программ (обычно дистрибутивы)c:\moe\vm
- виртуальные машиныТеперь финальный шаг - осуществление бэкапа. Мой бэкап не совсем “правильный”, так как он не содержит историю изменений, но зато он является моментальным снимком всех “моих” файлов. В случае проблемы или переезда на другой компьютер, я могу использовать бэкап как точную копию файловой системы и восстановить ее простым копированием. Кстати, это один из аргументов за аккуратное отделение “моих” файлов от прочего мусора.
Для физического осуществление бэкапа я использую nnbackup. Это весьма продвинутая программа, но я использую в ней только одну функцию односторонней синхронизации - при очередном сохранении каждый файл в каталоге c:\moe
, который изменился, будет обновлен. Но выходе, еще раз, точная копия этого каталога на страховочном носителе.
Под занавес, для редактирования переменных окружения в Windows я использую RapidEE. Сложно представить удобнее программу.
P.S. Дополнительный довесок (чтобы два раза не вставать), который вроде бы как по теме, но не совсем. В той же статье про Lion описывается, что Apple ввели понятие версионности документов на уровне API. Я видел похожее в одной из систем разработки в Блумберге - в IDE не надо было принудительно сохранять файлы проекта. В любой момент времени среда обеспечивает сохранность файлов, и в случае зависания или прочих проблем при перезапуске открытые файлы автоматически восстанавливались из autosave’а. А вот если разработчик принудительно сохраняет файл (например, нажимает Ctrl-S), то файл не просто сохраняется, а делается commit в SVN с новой ревизией, и также приходится вводить описание этого изменения.
Сначала меня такой подход бесил, так как многолетняя привычка в досовских “массировать” F2 или Ctrl-S каждые три секунды, приводила к окну ввода описания изменений. Но со временем, я реально подсел на это. Теперь Сохранение приобрело смысл, а не было лишь средством спасения от зависания в процесса запуска.
Я был приятно удивен, то в Lion схожий подход введен на уровне операционной системы. Тут, конечно, у Apple основная цель немного иная - они хотят тем самым полностью избавиться от вопросов о сохранении несохраненных документов при выходе из приложения, чтобы их (приложения), можно было незаметно для пользователя убивать и перезапускать заново, как в iOS. Забавно, что в Lion по умолчанию в Dock’е убрали индикатор запущенности приложения. Основной посыл такой - пользователя этот вопрос более не должен волновать. Операционная система запустит приложение, если требуется, и сама убьет его, если есть потребность в его ресурсах. Документы же, всегда будут в сохранности.
Кто еще поделится идеями и приемами организации файлов?
]]>Как там у Шекспира: “переписывать или не переписывать - вот в чем вопрос”. Можно приляпывать временные костыли там и сям, не давая зданию упасть, но придет время, когда законы физики возьмут свое, и здание таки нае*нется.
А можно на время вывести здание из эксплуатации и перестроить заново, например из кирпича.
]]>#include <string.h> #include <stdio.h> int main(int argc, char* argv[]) { char b[32]; strcpy(b, "123456789012345"); strcpy(b + 1, b); printf("[%s]\n", b); return 0; }
Тут ясно видна проблема - строки, передаваемые в strcpy()
, перекрываются.
По-хорошему, тут имеется неопределенное поведение, так как strcpy()
не гарантирует порядок перемещения байт, а именно от него зависит в данном случае результат.
Проверим на разных компиляторах и платформах.
Visual Studio 2010 64-bit
[1123446788012245]
Строка искажается каждые четыре байта, явно копировали по 32 бита.
Linux 64-bit
[1123456788012345]
Уже иной результат. Компилятор и libc:
ldd --version
ldd (GNU libc) 2.5
gcc --version
gcc (GCC) 4.1.2 20080704 (Red Hat 4.1.2-50)
В man strcpy
говорят:
*The strings may not overlap...*
Странно, почему не “must not”.
Solaris (SPARC)
[1123446788012245]
Компилятор и libc:
cc -V
cc: Sun C 5.8 2005/10/13
version /usr/lib/libC*
version of "/usr/lib/libC.so.3": SC2.0.1 12/20/94 Sun C++ 3.0.1 patch 100962-09
version of "/usr/lib/libC.so.5": Sun SUNWlibC SunOS 5.10 Patch 119963-06 2006/04/21
version of "/usr/lib/libCrun.so.1": Sun SUNWlibC SunOS 5.10 Patch 119963-06 2006/04/21
version of "/usr/lib/libCstd.so.1": Sun SUNWlibC SunOS 5.10 Patch 119963-06 2006/04/21
AIX
[1111111111012245]
Тут результат явно левый. Но зато в документации сказано ясно и понятно:
String movement is performed on a character-by-character basis and starts at the left. Overlapping moves toward the left work as expected, but overlapping moves to the right may give unexpected results.
Компилятор и libc:
lslpp -L | grep Compiler
vacpp.cmp.core 8.0.0.20 C F IBM XL C/C++ Compiler
lslpp -L | grep libc
bos.rte.libc 5.3.9.1 C F libc Library
HP-UX
[1123456789012345]
Компилятор:
what `which cc`
HP C/aC++ for Integrity Servers B3910B A.06.22 [Nov 14 2008]
Вроде скопировано правильно, но в документации (man strcpy
) говорят (формулировка интересна):
Character movement is performed differently in different implementations, so moves involving overlapping source and destination strings may yield surprises.
Вывод: strcpy()
- нехорошая функция, по многим причинам.
Если у кого есть подобные списки, прикладывайте ссылки в комментариях.
]]>Как результат надо вывести начальный и конечный индексы этого интервала.
Подразумевается решение O(n).
]]>Итак, рассмотрим пример (система AIX 5.3).
Файл alib.cpp
компилируем как динамическую библиотеку.
struct A { A() { value_ = 123; } int value_; }; A a = A(); extern "C" int value() { return a.value_; }
В этой библиотеке создается статический объект класса А
, и значение его поля возвращается функцией value()
.
Компилируем:
xlC -o alib.so -qrtti=all -qmkshrobj=-100 -G -brtl -bnolibpath alib.cpp
xlC
- это компилятор С++ на AIX.
Далее, файл main.c
. Это головной модуль на С
, который вызывает функцию value()
.
extern int value(); int main() { return value(); }
Этот модуль вызывает value()
, и значение становится кодом возврата процесса.
Компилируем:
xlc -c -o main.o main.c
xlc
(маленькая “с” на конце) - это компилятор С на AIX.
Линкуем, используя компилятор С, запускаем и печатаем код возврата ($?
)
xlc -o main main.o alib.so && LIBPATH=.:$LIBPATH ./main ; echo $?
Результат на экране:
0
Интересно?! Почему не ожидаемое 123?
Теперь линкуем, используя компилятор “С++”, запускаем и печатаем код возврата:
xlC -o main main.o alib.so && LIBPATH=.:$LIBPATH ./main ; echo $?
Результат на экране:
123
Мораль: на AIX, при динамической линковке библиотек, чтобы правильно работала статическая инициализация на С++, надо принудительно линковать конечный бинарь в режиме С++ (как бы это странно не звучало). Иначе конструкторы статических объектов вызваны не будут, и их инициализация будет произведена не ДО функции main()
, а непонятно когда.
Можно принудительно заставить таки систему вызвать конструкторы статических объектов, написав что-то вроде:
#include <dlfcn.h> static int module_initialised = 0; static void ManualInitilizationForStatics() { if (module_initialised) return; dlopen("blah.so", RTLD_NOW); module_initialised = 1; }
Но это не программирование, а ерзанье.
]]>EKOPath вроде как собирается выпустить в open-source их супер-пупер оптимизирующий компилятор C++.
Как пишут в пресс-релизе:
The PathScale EKOPath Compiler Suite has the world’s most advanced optimization infrastructure and can fully exploit the potentials of many-core architectures.
Вам хочется рисовать диаграммы и блок-схемы по-настоящему, в vi’e, роняя скупую слезу юниксоида, взгляните на это ASCII Flow.
Как говориться, почему рулит plain-text? А потому, что его можно поместить под контроль версий (1) и делать diff (2).
Google выложил в open-source Address Sanitizer.
Address Sanitizer - это аналог Valgrind, но не совсем. Например, эта штука умеет ловить buffer over-/underrun не только на куче, но и на стеке. Работает только на Linux Intel 32 и 64 бит.
]]>void f ( int a , char *p ) { int whattodo ; whattodo = a < 1 ? 2 : 0; switch ( whattodo ) { case 2: printf("THIS\n"); break; case 1: printf("THAT\n"); break; } if ( a != * p ) { a = 1111111 ; } else a = 2222222; }
Удивительно, но во многих компаниях, где я работал, подобный код спокойно подошел бы по принятому стандарту кодирования. И такой код вполне реален, так как он обычно создается за долгое время разными людьми. В целом код читаем и даже более менее понятен.
Но:
whattodo
{
и break
Вывод - это не код, а помойка.
Последнее время на любые аргументы по даже минимальным неаккуратностям в коде, а привожу сравнение с текстом для чтения.
Какой текст приятнее и понятнее читать?
Так:
Компьютерная сеть Международного валютного фонда подверглась хакерской атаке. Официального объявления об этом пока не сделано, однако сотрудникам о вторжении сообщили еще в среду. Представитель МВФ отказался сообщить какую-либо информацию об инциденте, заверив журналистов, что организация работает в обычном режиме.
Или так:
кОМИьютерная сеть Международного валютного
фонда подверглась хакерской атаке . Официального
объявления об этом пока не сделано , однако сотрудникам о
вторжении сообщили ЕЩе в СрЕдУ.
ПредстАвитель МВФ отказался сООбщить какую- либо информацию об
инциденте , заверив журналистов , что организация работает в
обычном реЖиме .
На многих это производит эффект.
Даже в крупных компаниях, когда вроде есть и время и люди, которые занимаются поддержкой качества кодовый базы, бывает трудно справляться с этой задачей. Когда появляются аргументы типа “клиент хочет это сегодня”, то мантра “ну так работает же!!!” перебивает все аргументы за тотальную аккуратность в коде. Увы.
Еще пару мыслей. Есть определенные приемы в форматировании кода, которые призваны облегчить copy-paste.
Например расположение запятых на следующей строке. Это позволяет менять порядок строк без необходимости думать о запятой в последней строке:
A::A() :
a(1)
, b(1)
, c(1)
{
...
}
Лично я не против copy-paste, когда ради его удобства уродуют текст - это уже слишком. Я резко против таких приемов.
Кстати, один из аргументов, почему люди любят ставить открывающую фигурную скобку на отдельной строке, как этом примере, потому что так удобнее таскать это по тексту строчными блоками и не думать даже о минимальной корректировке при вставке в другое месте.
Ошибки, вызванные такими легко и слепо перетаскиваемыми блоками, выльются в гораздо большие временные затраты, чем пару лишних секунд на ручную адаптацию перенесенного блока, когда за эти пару секунд ты так или иначе будешь вынужден пробежать глазами по тексту, и, возможно, выловить логические несовместимости.
]]>Первое, что приходит в голову – задача выдачи денег в банкомате.
Клиент хочет сумму X, и в банкомате есть купюры нескольких номиналов. Если выбранная целевая функция – минимизировать количество выдаваемых купюр, то задача сводится к распространённой задаче выдачи сдачи и решается за O(N^2).
Но чаще выбирают иную целевую функцию – максимизировать интервалы между инкассациями. Для этого надо выдавать купюры так, чтобы их расход из каждой кассете был равномерный. Тут уже задачу надо решать в общем виде как оптимизационную задачу целочисленного программирования. Есть целевая функция, есть ограничения, и задача решается обычно какими-либо около переборными методами с различными эвристиками.
Но увы – данную задачу для банкомата при реальных ограничениях проще решать в лоб, перебором. Хотелось бы применить что-то интересное, но заканчиваем банальным брутфорсом.
Ещё мне приходилось реализовывать простейшие примитивы для рисования фигур на плоскости. Например, есть интересный алгоритм Брезенхэма, которым можно рисовать линии без применения вещественной арифметики (и тем более без всяких синусов и косинусов).
И увы, мой список на этом заканчивается.
А какие прикольные алгоритмы приходилось в реальной работе использовать вам?
]]>Ниже очередная пузомерка сферического коня в попугаях.
В забеге учавствуют:
std::string::find()
std::strstr()
naive_strstr()
со сложностью O(N^2)
kmp_strstr()
) без особых изысковДанные для поиска сделаны в виде “наихудщего случая”, когда сравнивать надо все до победного, и совпадение будет только с самом конце. Это должно вызвать явное квадратичное время у naive_strstr()
.
#include <iostream> #include <vector> #include <cstdlib> #include <cstring> #include <cassert> #include <ctime> int naive_strstr(const char* str, const char* needle) { int str_sz = std::strlen(str); int needle_sz = std::strlen(needle); for (int i = 0; i < str_sz - needle_sz + 1; ++i) { int j; for (j = 0; j < needle_sz; ++j) if (str[i + j] != needle[j]) break; if (j == needle_sz) return i; } return -1; } int kmp_strstr(const char* str, const char* needle) { int str_sz = std::strlen(str); int needle_sz = std::strlen(needle); std::vector<int> prefix(needle_sz, 0); for (int i = 1; i < needle_sz; ++i) { int j = prefix[i - 1]; while (j > 0 && needle[j] != needle[i]) j = prefix[j - 1]; if (needle[j] == needle[i]) j += 1; prefix[i] = j; } int j = 0; for (int i = 0; i < str_sz; ++i) { while (j > 0 && needle[j] != str[i]) j = prefix[j - 1]; if (needle[j] == str[i]) j += 1; if (j == needle_sz) return i - j + 1; } return -1; } int main(int argc, char* argv[]) { int N = argc > 1 ? std::atoi(argv[1]) : 10*1000*1000; int M = argc > 2 ? std::atoi(argv[2]) : 1000; std::string str(N, 'a'); std::string needle(M, 'a'); // Our needle is the last M characters of the string. str[str.length() - 1] += 1; needle[needle.length() - 1] += 1; std::cout << "N = " << N << ", M = " << M << std::endl; size_t correct_position = str.length() - needle.length(); std::cout << "Correct position: " << correct_position << std::endl; const char* str_p = str.c_str(); assert(std::strlen(str_p) == str.length()); const char* needle_p = needle.c_str(); assert(std::strlen(needle_p) == needle.length()); time_t started, duration; size_t i; started = std::time(0); i = str.find(needle); duration = std::time(0)- started; std::cout << "string::find(): " << i << ", time " << duration << std::endl; assert(i == correct_position); started = std::time(0); const char* p = std::strstr(str_p, needle_p); duration = std::time(0)- started; assert(p != NULL); i = p - str_p; std::cout << "strstr() : " << i << ", time " << duration << std::endl; assert(i == correct_position); started = std::time(0); i = naive_strstr(str_p, needle_p); duration = std::time(0)- started; std::cout << "naive_strstr(): " << i << ", time " << duration << std::endl; assert(i == correct_position); started = std::time(0); i = kmp_strstr(str_p, needle_p); duration = std::time(0)- started; std::cout << "kmp_strstr() : " << i << ", time " << duration << std::endl; assert(i == correct_position); return 0; }
Makefile:
all: do-32 do-64 target = strstr_find do-32: build-32 $(target) do-64: build-64 $(target) do-build: "%VS100COMNTOOLS%..\..\VC\vcvarsall.bat" $(arch) && cl /O2 /EHsc $(target).cpp build-32: $(MAKE) do-build arch=x86 build-64: $(MAKE) do-build arch=amd64 run: $(target) clean: -del *.exe *.ilk *.obj *.pdb *.suo
Запускаем на Visual Studio 2010 32-bit:
N = 10000000, M = 1000
Correct position: 9999000
string::find(): 9999000, time 4
strstr() : 9999000, time 8
naive_strstr(): 9999000, time 8
kmp_strstr() : 9999000, time 0
Запускаем на Visual Studio 2010 64-bit и получаем странное ускорение у find()
и замедление strstr()
и naive_strstr()
:
N = 10000000, M = 1000
Correct position: 9999000
string::find(): 9999000, time 1
strstr() : 9999000, time 16
naive_strstr(): 9999000, time 10
kmp_strstr() : 9999000, time 0
Конечно, тут есть много тонкостей. При различных данных в среднем strstr()
может реально обгонять мою реализацию КМП, так как strstr()
все-таки написана на ассемблере, и накладные расходы в КМП могут съесть всего его преимущества, но вот если данные всегда будут “плохими”, то без КМП не обойдить.
И еще. Так как КМП требует дополнительную память порядка длины искомой строки, то подобное осложнение может не годиться для библиотечной функции широкого применения. Может поэтому strstr()
и string::find()
и работают “в лоб”.
Одно не понятно - почему string::find()
быстрее strstr()
? Может из-за тотального inline’а.
const char* a = ::getenv("VAR"); if (a == NULL) a = "[NULL]";
Но в каком-то внутреннем стремлении сделать код “немного лучше”, можно написать так:
const char* a = (a = ::getenv("VAR"), a ? a : "[NULL]");
Мне внутренне больше нравиться второй вариант. Он как-то выразительнее. Но на code view я, конечно, буду настаивать однозначно на первом, так как не только мне одному этот код потом читать.
Также в С++ считается хорошим тоном объявлять переменную цикла for
прямо в теле оператора:
for (int i = 0; ...
Но вот конструкции типа:
if (int a = foo()) { ... }
или
while (int a = foo()) { ... }
уже немного режут глаз, хотя и являются корректными.
А у вас какие есть прикольные сокращение? пусть даже не для production code?
]]>#include <iostream> int ct = 1; struct G { ~G() { ct--; } }; int main() { (G()); // (1) std::cout << ct << std::endl; return 0; }
Как вы думаете - что напечатает данная программа?
Весь вопрос в том, когда будет вызван деструктор временного объекта, созданного в строке (1)
: сразу после знака ;
в этой же строке или в конце блока на символе }
?
Если первое, то программа выведет “0”, если второе, то “1”.
Я проверил на 6 разных компиляторах на разных платформах - везде печатается “0”, что соответствует стандарту.
Но на отдельно выделенной версии Sun C++ 5.8 2005⁄10/13 данная программа печатает “1”!
Мы провели с коллегой интересные пару часов, пытаясь в огромной программe локализовать этот коварный глюк.
]]>Вот одна из них.
Имеется n винтов и n гнезд, расположенных в произвольном порядке. Каждому винту соответствует по диаметру только одно гнездо. Все винты имеют разные диаметры.
Требуется расставить все винты по гнездам. Разрешено только одно действие - попытка вставить винт i в гнездо j. В результате такой операции можно выяснить: (1) винт тоньще гнезда - не подходит, (2) винт толще гнезда - не подходит, (3) или винт точно входит в гнездо - подходит.
Сравнивать винты или гнезда между собой нельзя.
Очевидное решение за O(N^2) - попробовать каждый болт последовательно в каждое гнездо до совпадения. Но есть решение быстрее.
]]>Например, это можно слелать так:
#define arraysize(array) (sizeof(array) / sizeof(array[0]))
Но тут есть одна проблема. Если случайно передать в этот макрос не массив, а просто указатель, что ошибки компиляции не будет, но значение будет далеко от задуманного.
Вчера прочитал на Харбе (кстати, отличная статья), что в С++ можно сделать этот макрос более безопасным.
Вот код, который используется в Chrome:
template <typename T, size_t N> char (&ArraySizeHelper(T (&array)[N]))[N]; #define arraysize(array) (sizeof(ArraySizeHelper(array)))
Выглядит немного запутанно, но можно разобраться:
T (&array)[N]
- определение массива (T array[N]
), который передается по ссылкеchar (&ArraySizeHelper(...)[N]
- функция, возвращающая массив по ссылкеsizeof(ArraySizeHelper(array))
- определение размера возвращаемого функцией значенияЕсли честно, додуматься до такого непросто. Но макрос весьма хорош. Я взял себе на вооружение.
Кстати, можно поиграться с sizeof()
от типа возвращаемого функцией значения:
#include <iostream> #include <string> std::string f() { return std::string(); } int main() { std::cout << sizeof( (&f)() ) << std::endl; std::cout << sizeof( std::string ) << std::endl; return 0; }
У меня на VS2010 выводит два раза число “28”.
Интересно, что в чистом С такой номер тоже проходит:
#include <stdio.h> struct t { char x[1024]; }; struct t f() { struct t a; return a; } int main() { printf("%d\n", sizeof(struct t)); printf("%d\n", sizeof( (*f)() )); return 0; }
Печатает два раза “1024”.
]]>Итак, задача с крайне простой постановкой.
Дан произвольный двудольный граф: “n” вершин слева, “m” справа и некоторое количество дуг. Требуется ответить на вопрос: сколько раз дуги графа пересекаются.
В данном примере n=5, m=4, десять дуг: 1-1, 1-2, 2-1, 2-2, 3-3, 4-1, 4-3, 5-1, 5-2, 5-4, количество пересечений: 10.
Факт пересения рассматривается всегда попарно. Например, если уж так случилось, и три дуги пересекаются в одной геометрической точке, формально пересечений все равно три, а не одно.
Задача имеет решение за O(n*m).
]]>#include <stdio.h> int main() { int i; for (i = 0; i < 8; ++i) printf("%c", "12345678"[i]); printf("\n"); return 0; }
Лично мне выражение "12345678"[i]
как-то режет глаз. Хотя с точки зрения языка тут все в порядке.
Посмотрите, как прямо в вашем браузере загрузится Linux. Запустите ls, top, emacs, vi, df, ifconfig, ping и т.д. Попробуйте скомпилировать и запустить программу tcc hello.c && ./a.out
.
А теперь осознайте - это все чистая программная эмуляция x86 (подробности).
Мой эмулятор Intel 8080 и Радио-86РК просто ничто, по сравнению с этим.
У меня до сих пор небольшой шок от увиденного.
Когда-то я был уже удивлен до глубины души одним из проектов (Загрузка Linux без ядра за 25 секунд) этого товарища, но это…
]]>Но сегодня, открыв “Windows Internals” в случайном месте, наткнулся на параграф про данный вопрос.
Итак, просто привожу небольшой лог с консоли (Windows 7).
ver
Microsoft Windows [Version 6.1.7601]
Создаем файл и каталог:
cd C:\Temp\links
C:\temp\links>mkdir folder
C:\temp\links>echo >file
Создаем символьную ссылку на каталог:
C:\temp\links>mklink /D link1 folder
symbolic link created for link1 <<===>> folder
Создаем junction на каталог (на файл его создать нельзя):
C:\temp\links>mklink /J link2 folder
Junction created for link2 <<===>> folder
Создаем символьную ссылку на каталог немного иначе:
C:\temp\links>mklink link3 folder
symbolic link created for link3 <<===>> folder
Создаем символьную ссылку на файл:
C:\temp\links>mklink link4 file
symbolic link created for link4 <<===>> file
Результат:
C:\temp\links>dir
Volume in drive C has no label.
Volume Serial Number is C021-6C9F
Directory of C:\temp\links
09/05/2011 18:26 <DIR> .
09/05/2011 18:26 <DIR> ..
09/05/2011 18:26 13 file
09/05/2011 18:25 <SYMLINKD> link1 [folder]
09/05/2011 18:25 <JUNCTION> link2 [C:\temp\links\folder]
09/05/2011 18:25 <SYMLINK> link3 [folder]
09/05/2011 18:26 <SYMLINK> link4 [file]
09/05/2011 18:23 <DIR> folder
3 File(s) 13 bytes
5 Dir(s) 208,278,925,312 bytes free
Обратите внимание на интересные типы файлов: <SYMLINKD>
, <JUNCTION>
, <SYMLINK>
. Как написано в книге, первые два по функциональности одно и то же, просто <JUNCTION>
более старый механизм, доступный в более старых версиях Windows и который поддерживает ссылки только внутри одного тома.
Также, обратите внимание, что ссылка link3
хоть и является ссылкой на каталог, не работает нормально как обычный каталог (в отличие от link1
и link2
, которые в целом ведут себя как нормальне каталоги). FAR, кстати, тоже, link3
за каталог не считает.
В общем, такая простая задача как ссылки внутри файловой системы, решенная в UNIXах более двадцати лет назад, решена в Windows традиционным для этой операционной системы путем - решений много, и каждое имеет свой уровень совместимости.
И книга, “Windows Internals”, чертовски хороша, рекомендую.
]]>Скотт Мейерс
Move semantics, perfect forwarding, and rvalue references
Джон Лакос (Блумберг)
Defensive programming done right
Диетмар Кул (Блумберг)
Generic Programming with C++ 0x
Остальные видео тоже доступны.
]]>Все как обычно – стараемся приучать к культуре разработки через тесты, ну и попутно склонить патриотов С к С++, убедив их, что на С++ можно таки писать также эффективно, как и на С. Да еще и в разы быстрее.
Зашла тема про 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')
.
А разница весьма существенна.
Посты по теме:
]]>Но что ни говори, для локальной работы, когда утопаешь в экспериментах, тестовых данных, временных копиях и т.д. - наличие распределенной системы типа git, hg, bazaar или fossil, когда можно плодить ветки по каждому чиху, сливать, удалять и т.д., реально упрощает жизнь.
Как рекоммендуют сами Perforce’овцы, можно в некотором роде срастить оба подхода, например, через git.
p4-git - скрипт, которым локальные файлы, находящиеся под контролем Perforce, дополнительно можно взять под git.
Я все настроил, как сказано. Теперь у меня в git есть ветка master, являющаяся зеркалом из репозитория Perforce, и пяток-десяток локальных веток, из которых я сливаю в master. Изменения, которые я внес через git, автоматически заливаются в Perforce командой git p4 submit
. Комадной же git p4 rebase
можно синхронизировать ветку master с ее оригиналом в Perforce.
Кстати, я уже потерял счет тем разам, когда в hg или fossil’e я влеплял ошибку в команде комита - либо просто опечатка в сообщении, что еще можно пережить, или при повторении команд из буфера командой строки вместо diff
залепишь старый комит и все. Потом приходится либо как-то хитро merge’ить, либо просто откатывать изменения, делая новый комит. А в git можно просто сказать git commit --amend
для исправления опечатки в только что сделанном комите, или git reset HEAD^1
для удаления последнего комита вообще. А меняя 1 на 2, 3 и т.д., можно удалить сколько угодно комитов назад.
А самое важное, что даже неверная команда git reset HEAD^n
, которая якобы удаляет n последних комитов - это не конец света. И ее можно откатить через git reset <commit_id>
, где <commit_id>
- это идентификатор удаленного комита. При всех тех возможностях по работе с репозиторием, которые дает git, и которые принято считать “опасными”, очень мало команд, которые реально имеют необратимые последствия. Пока вы не сделали сборку мусора командой git repack
объекты физически не удаляются, а только меняются указатели на них, а значит практически всегда можно вернуть назад, когда напортачил.
Характерный пример - компания Hex-Rays.
Их главный продукт - интерактивный дизассемблер IDA Pro. Хотя сейчас это уже не просто дизассемблер, а еще и отладчик и декомпилятор.
В общем, продукт, на который, как я думаю, молятся аналитики по компьютерной безопасности, авторы антивирусов и систем защит от копирования, а также люди с другой стороны баррикад - авторы вирусов и взломщики защит.
IDA уникален по функциональности, удобству и количеству поддерживаемых архитектур. А его декомпилятор, превращающий ассемблерный код в псевдокод на С, вообще не имеет себе аналогов. Рекомендую посмотреть видео с демонстрацией работы декомпилятора. Это впечатляет.
Неделю назад в блоге компании появилось объявление о поиске разработчиков, желающих присоединиться к их небольшой, но очень продуктивной команде. Компания базируется в Бельгии, но скорее всего варианты сотрудничества могут быть весьма гибкими, от переезда в Бельгию до удаленной работы. В компании, кстати, также говорят по-русски.
Итак, любите reverse engineering и ассемблер, и при этом умеете грамотно писать на С++ - не пропустите.
P.S. Предвосхищу вопросы на тему какой-либо моей личной финансовой заинтересованности в публикации данной информации, что пост никак официально не связан с компанией Hex-Rays и является лишь моим личным мнением, высказанным только из-за многолетней привязанности к продукту IDA.
]]>А что может быть важнее, когда член команды очень ценит и гордится членством - ничего! Особенно, например, для армии, когда от преданности сослуживца группе может зависеть жизнь остальных. Поэтому (в книге это объяснялось) все попытки административно запретить посвящение в морпеха или крапового берета, или день студента, когда злобно издеваются над новобранцами в студентческие братства, обречены на провал. Будет запрет - будут делать нелегально (историей это подтверждается), так как это животная природа человека к самосохранению в группе.
Ладно, тут можно много спорить. Желающие прочтут в книге сами.
Сейчас не об этом. Я тут подумал, что жесткие процедуры интервьюирования отчасти работают по такому же принципу. Пройдя подобный отбор, кто в душе не будет говорить себе: “Да! Я прошел! Теперь я член этой классной команды!”?
Возразите мне…
]]>Например, flex - это один из таких анализаторов. Старый, но проверенный годами.
Я много пользовался flex’ом, он имеет и плохие и хорошие стороны, но по большому счету, жаловаться не приходилось.
Но вчера наткнулся на интересный проект - re2c. По сути, на этой штуке можно писать лексические анализаторы прямо на коленке за несколько минут.
Сразу рассмотрим пример.
Допустим, вам нужно из строки выделять некоторые команды, целые и дробные числа. Можно расчехлить flex, а можно написать так:
#include <stdio.h> #include <stdlib.h> enum { CMD, INT, FLOAT, SPACE, END }; int scan(char** p, char** lex) { char* marker; if (lex) *lex = *p; /*!re2c re2c:define:YYCTYPE = "unsigned char"; re2c:define:YYCURSOR = *p; re2c:define:YYMARKER = marker; re2c:yyfill:enable = 0; re2c:yych:conversion = 1; re2c:indent:top = 1; "GET"|"PUT"|"EXIT" { return CMD; } [0-9]+ { return INT; } [0-9]+ '.' [0-9]* { return FLOAT; } [ \t]+ { return SPACE; } [^] { return END; } */ } int main(int argc, char* argv[]) { char *p, *last; int token; if (argc < 2) return 1; p = argv[1]; while ((token = scan(&p, &last)) != END) { int sz = p - last; switch (token) { case CMD: printf("Command: '%.*s'\n", sz, last); break; case INT: printf("Number: '%.*s'\n", sz, last); break; case FLOAT: printf("Float: '%.*s'\n", sz, last); break; } } return 0; }
И все!
Понятно, что вся магия происходит в функции scan()
между строками, ограниченных комментариями /*!re2c
и */
.
Итак, re2c - это компилятор регулярных выражений, который встраивает код прямо в текст программы.
Если прогнать наш исходник через re2c:
re2c.exe -is test.re2c >test.c
То получим вот такое:
/* Generated by re2c 0.13.5 on Tue Apr 19 21:08:57 2011 */ #include <stdio.h> #include <stdlib.h> enum { CMD, INT, FLOAT, SPACE, END }; int scan(char** p, char** lex) { char* marker; if (lex) *lex = *p; { unsigned char yych; yych = (unsigned char)**p; if (yych <= '9') { if (yych <= 0x1F) { if (yych == '\t') goto yy8; goto yy10; } else { if (yych <= ' ') goto yy8; if (yych <= '/') goto yy10; goto yy6; } } else { if (yych <= 'F') { if (yych == 'E') goto yy5; goto yy10; } else { if (yych <= 'G') goto yy2; if (yych == 'P') goto yy4; goto yy10; } } yy2: yych = (unsigned char)*(marker = ++*p); if (yych == 'E') goto yy24; yy3: { return END; } yy4: yych = (unsigned char)*(marker = ++*p); if (yych == 'U') goto yy23; goto yy3; yy5: yych = (unsigned char)*(marker = ++*p); if (yych == 'X') goto yy18; goto yy3; yy6: ++*p; if ((yych = (unsigned char)**p) == '.') goto yy13; if (yych <= '/') goto yy7; if (yych <= '9') goto yy16; yy7: { return INT; } yy8: ++*p; yych = (unsigned char)**p; goto yy12; yy9: { return SPACE; } yy10: yych = (unsigned char)*++*p; goto yy3; yy11: ++*p; yych = (unsigned char)**p; yy12: if (yych == '\t') goto yy11; if (yych == ' ') goto yy11; goto yy9; yy13: ++*p; yych = (unsigned char)**p; if (yych <= '/') goto yy15; if (yych <= '9') goto yy13; yy15: { return FLOAT; } yy16: ++*p; yych = (unsigned char)**p; if (yych == '.') goto yy13; if (yych <= '/') goto yy7; if (yych <= '9') goto yy16; goto yy7; yy18: yych = (unsigned char)*++*p; if (yych == 'I') goto yy20; yy19: *p = marker; goto yy3; yy20: yych = (unsigned char)*++*p; if (yych != 'T') goto yy19; yy21: ++*p; { return CMD; } yy23: yych = (unsigned char)*++*p; if (yych == 'T') goto yy21; goto yy19; yy24: ++*p; if ((yych = (unsigned char)**p) == 'T') goto yy21; goto yy19; } } int main(int argc, char* argv[]) { char *p, *last; int token; if (argc < 2) return 1; p = argv[1]; while ((token = scan(&p, &last)) != END) { int sz = p - last; switch (token) { case CMD: printf("Command: '%.*s'\n", sz, last); break; case INT: printf("Number: '%.*s'\n", sz, last); break; case FLOAT: printf("Float: '%.*s'\n", sz, last); break; } } return 0; }
Страшно? Да, код не для ручной правки, но это и не требуется.
Компилируем:
re2c.exe -is test.re2c >test.c && cl test.c
Запускаем:
test "GET 123.0 12344 PUT 10."
Результат:
Command: 'GET'
Float: '123.0'
Number: '12344'
Command: 'PUT'
Float: '10.'
Как говориться, быстро, дешево и сердито. Чтобы полностью овладеть re2c надо прочитать одну и единственную страничку документации.
Кстати, простота работы с re2c не означает, что на нем нельзя делать сложных анализаторов. В дистрибутиве есть примеры для грамматики токенов языков C и Rexx.
Если поиграться с флагами re2c, то можно вместо if/else
генерировать код на основе switch/case
. Выбор стоит сделать на основе понимания, какой код ваш компилятор лучше оптимизирует.
Как я понимаю, анализатор, сгенерированный re2c должен быть весьма быстр, даже очень быстр. Хотя было бы интересно померить его против того же flex, ANTLR или Spirit.
Посты почти по теме:
]]>Признаюсь, я не боец в бусте и новом C++, но благодаря предоставленному примеру, было очевидно, что и на С++ решение получается весьма изящное.
Интересно было сравнить производительнось потоков во обоих языках в плане скорости из создания и назначения им работы. Как я понял, это битва между pthreads и системой Go-рутин, которые не являются потоками операционной системы. Как сказано в документации:
Goroutines are multiplexed onto multiple OS threads so if one should block, such as while waiting for I/O, others continue to run. Their design hides many of the complexities of thread creation and management.
Я взял последний boost, и на той же восьми процессорной машине провел эксперимент.
Программе надо будет выполнить множество однотипной работы (фактически, вызвать функцию). Задачи будут мультиплексироваться между несколькими параллельными потоками. Сама функция будет элементарной и быстрой. Надеюсь, этим удастся сфокусировать тестирование именно на подсистеме потоков, нежели на полезной нагрузке.
Итак, программа на Go:
package main import ( "flag" "fmt" ) var jobs *int = flag.Int("jobs", 8, "number of concurrent jobs") var n *int = flag.Int("tasks", 1000000, "number of tasks") func main() { flag.Parse() fmt.Printf("- running %d concurrent job(s)\n", *jobs) fmt.Printf("- running %d tasks\n", *n) tasks := make(chan int, *jobs) done := make(chan bool) for i := 0; i < *jobs; i++ { go runner(tasks, done) } for i := 1; i <= *n; i++ { tasks <- i } for i := 0; i < *jobs; i++ { tasks <- 0 <- done } } func runner(tasks chan int, done chan bool) { for { if arg := <- tasks; arg == 0 { break } worker() } done <- true } func worker() int { return 0 }
Makefile для прогона по серии параметров:
target = go_threading all: build build: 6g $(target).go 6l -o $(target) $(target).6 run: (time -p ./$(target) -tasks=$(args) \ 1>/dev/null) 2>&1 | head -1 | awk '{ print $$2 }' n = \ 10000 \ 100000 \ 1000000 \ 10000000 \ 100000000 test: @for i in $(n); do \ echo "`printf '% 10d' $$i`" `$(MAKE) args=$$i run`; \ done
Программа на C++:
#include <iostream> #include <boost/thread.hpp> #include <boost/bind.hpp> #include <queue> #include <string> #include <sstream> class thread_pool { typedef boost::function0<void> worker; boost::thread_group threads_; std::queue<worker> queue_; boost::mutex mutex_; boost::condition_variable cv_; bool done_; public: thread_pool() : done_(false) { for(int i = 0; i < boost::thread::hardware_concurrency(); ++i) threads_.create_thread(boost::bind(&thread_pool::run, this)); } void join() { threads_.join_all(); } void run() { while (true) { worker job; { boost::mutex::scoped_lock lock(mutex_); while (queue_.empty() && !done_) cv_.wait(lock); if (queue_.empty() && done_) return; job = queue_.front(); queue_.pop(); } execute(job); } } void execute(const worker& job) { job(); } void add(const worker& job) { boost::mutex::scoped_lock lock(mutex_); queue_.push(job); cv_.notify_one(); } void finish() { boost::mutex::scoped_lock lock(mutex_); done_ = true; cv_.notify_all(); } }; void task() { volatile int r = 0; } int main(int argc, char* argv[]) { thread_pool pool; int n = argc > 1 ? std::atoi(argv[1]) : 10000; int threads = boost::thread::hardware_concurrency(); std::cout << "- executing " << threads << " concurrent job(s)" << std::endl; std::cout << "- running " << n << " tasks" << std::endl; for (int i = 0; i < n; ++i) { pool.add(task); } pool.finish(); pool.join(); return 0; }
Makefile:
BOOST = ~/opt/boost-1.46.1 target = boost_threading build: g++ -O2 -I $(BOOST) -o $(target) \ -lpthread \ -lboost_thread \ -L $(BOOST)/stage/lib \ $(target).cpp run: (time -p LD_LIBRARY_PATH=$(BOOST)/stage/lib ./$(target) $(args) \ 1>/dev/null) 2>&1 | head -1 | awk '{ print $$2 }' n = \ 10000 \ 100000 \ 1000000 \ 10000000 \ 100000000 test: @for i in $(n); do \ echo "`printf '% 10d' $$i`" `$(MAKE) args=$$i run`; \ done
В обоих языках число потоков будет равно количеству процессоров - 8. Количество задач, прогоняемых через эти восемь поток будет варьироваться.
Запускаем программу на C++:
make && make -s test
g++ -O2 -I ~/opt/boost-1.46.1 -o boost_threading \
-lpthread \
-lboost_thread \
-L ~/opt/boost-1.46.1/stage/lib \
boost_threading.cpp
(time -p LD_LIBRARY_PATH=~/opt/boost-1.46.1/stage/lib ./boost_threading \
1>/dev/null) 2>&1 | head -1 | awk '{ print $2 }'
10000 0.03
100000 0.35
1000000 3.43
10000000 29.57
100000000 327.37
Теперь Go:
make && make -s test
6g go_threading.go
6l -o go_threading go_threading.6
10000 0.00
100000 0.03
1000000 0.35
10000000 3.72
100000000 38.27
Разница очевидна.
Может быть я сравниваю соленое с красным, и результаты просто неадекватны. Будет очень признателен за подсказку, в каких попугаях на правильно измерять.
Посты по теме:
]]>Вот список изменений:
Полная история версий Google Test.
Каких-то радикально новых возможностей вроде нет, но исправлено несколько неприятных багов.
Лично я очень ждал исправления мелких, но неприятных несовместимостей с компиляторами HP-UX, Sun и AIX.
Посты по теме и почти по теме:
UPDATE
По ходу вышел еще и Google Mock 1.6.0.
Что нового тут:
Например, ACCU 2011. Место проведения от моего офиса всего в часе езды, не пришлось платить за гостиницу.
Тематики сессий весьма разнообразные.
Сегодня я посетил:
Introduction to Scala. До этого я про язык Scala только название, поэтому это были весьма полезные полтора часа. Если вы любите Java, но вам не хватает функциональных примочек типа списковых выражений, currying, pattern matching, call by name и более краткого синтаксиса в целом, но при этом вы хотите иметь обьектно- и функционально- ориентированный язык, компилирующий для JVM, то Scala - это самое оно. Хотя лично у меня впечатление неоднозначное из-за дикой перегруженности языка наворотами, из-за которой для “правильного программирования” надо также овладеть “правильными шаблонами”. А тут уже какой-то С++ получается. Сама презентация была весьма живая и информативная. Для интересующихся - блог автора.
Worst practices for creating domain-specific modelling languages. Не самая захватывающая, но таки полезная презентация о том, почему и как создаются проблемно ориентированные языки.
GoLightly: Building VM-based Language Runtimes In Go. Меня презентация реально вставила. Тетка отожгла неимоверно. За полтора часа она умудрилась дать очень понятную вводную по ключевым возможностям Go, успев даже коснуться недокументированных модулей типа “unsafe” и показать как можно обойти стройную систему безопасной типизации в Go, и затем описала свой проект по моделированию разных процессорных архитектур на этом языке. Ниже есть слайды ее презентации, но к сожалению в них нет той части про надругательноство на системой типов в Go. Может она выложит обновленную версию сегодня-завтра.
Завтра хочу сходить на “Distributed Computing 2.0”, “ComputErl - Erlang-based Framework for Many Task Computing”, “CPU Caches and Why You Care” Скотта Меерса.
Кстати, чтобы уж два раза не вставать, в последнем выпуске ACCU’шного журнала CVu была моя статейка “The First Little Step into Test-Driven Development” (так как электронная версия журнала находится в платной части сайта, то я выложил только страницы с моей статьей).
]]>К сожалению, по историческим причинам, у нас не было четкого регрессионного тестирования для этого компилятора. Но сейчас, на основе исходников бизнес-приложения, написанного на этом бейсике, решили сделать полноценное тестирование.
План таков: принять какую-то текущую версию компилятора, на которую нет открытых жалоб от клиентов, за эталон. Скомпилировать этой версией приличное количество исходников, сохранить результат, и затем каждый раз при внесении в компилятор изменений прогонять все эти исходники и смотреть, генерируется ли точно такой же вывод. Это не защитит от появления ошибок в целом, но по крайне мере будет уверенность, что существующий бизнес код все еще компилируется правильно.
Несложная задача. Только есть одно “но”. Количество исходников, которые планируется использовать как эталонные - около 15 тысяч файлов, суммарным объемом чуть меньше гига (для удобства они завернуты в один TAR). Подобный “прогон” может быть весьма долгим. И есть естественное желание сделать тест максимально быстрым, используя многопроцессорную машину, ибо задача прекрасно распараллеливается.
Как вариант - можно сделать Makefile и запускать его с ключом -j
в GNU Make. Но если написать специализированную многопоточную программу, то можно достичь лучшей производительности.
Итак, очевидно: вместо последовательного выполнения нужно запускать компиляцию каждого файла в параллельных потоках. Но так как файлов много (~15 тысяч), неэффективно просто одновременно запустить столько много потоков. Разумнее всего будет иметь пул потоков, где их количество будет определяться, например, количеством процессоров (например, умноженное на 2). Пул будет назначать очередную задачу на свободный поток, и если все потоки заняты, он будет блокироваться до тех пор, пока не появиться свободный.
Таким образом, мы будем поддерживать занятыми N потоков, обеспечивая оптимальную загрузку процессоров, не тратя время на лишние переключения контекста и постоянное создание и уничтожение потоков.
Сначала я решил написать все на С++ и pthreads. После нескольких часов танцов вокруг функторов, мьютексов, семафоров и условных переменных, у меня так ничего реально работающего не вышло. И тут я вспомнил про Go. Не поверите - через час работы у меня была готова первая версия, включая мелочевку типа работы с TAR, командной строкой и запуском внешнего процесса.
Итак: данная программа берет TAR с исходниками, распаковывает его, и каждый файл прогоняет через компилятор.
Сразу скажу, цель того, что я все это пишу тут, это продемонстрировать (и не более того), как просто и удобно на Go можно писать многопоточные императивные программы.
Главная концепция, которая используется в этой программе - это каналы. По каналам можно синхронно передавать данные и функции между потоками (Go-рутинами).
Далее, можно просто смотреть по исходнику. Самое интересное место там, где видно, как функция “compile()” может вызываться из нескольких потоков без каких-либо изменений.
package main import ( "archive/tar" "container/vector" "exec" "flag" "fmt" "io" "os" "strings" ) // Два флага: количество потоков и имя компилятора. var jobs *int = flag.Int("jobs", 0, "number of concurrent jobs") var compiler *string = flag.String("cc", "bcom", "compiler name") func main() { flag.Parse() os.Args = flag.Args() args := os.Args ar := args[0] r, err := os.Open(ar, os.O_RDONLY, 0666); if err != nil { fmt.Printf("unable to open TAR %s\n", ar) os.Exit(1) } // defer - это аналог "finally {}", гарантированное выполнение // кода при выходе из блока. defer r.Close() // Цикл распаковки TAR. fmt.Printf("- extracting %s\n", ar) // Создаем контекст для распаковки. tr := tar.NewReader(r) tests := new(vector.StringVector) // Последовательный проход по архиву, сохранение файлов и составление // списка для компиляции. for { // Получаем дескриптор следующего файла в архиве. hdr, _ := tr.Next() if hdr == nil { break } name := &hdr.Name // Если это не заголовочный файл, сохраним имя. if !strings.HasPrefix(*name, "HDR_") { tests.Push(*name) } // Создаем новый файл. w, err := os.Open("data/" + *name, os.O_CREAT | os.O_RDWR, 0666) if err != nil { fmt.Printf("unable to create %s\n", *name) os.Exit(1) } // Копируем содержимое в текущий файл. io.Copy(w, tr) w.Close() } fmt.Printf("- compiling...\n") *compiler , _ = exec.LookPath(*compiler) fmt.Printf("- compiler %s\n", *compiler) if *jobs == 0 { // Вызываем "compile()" последовательно, в основном потоке. fmt.Printf("- running sequentially\n") for i := 0; i < tests.Len(); i++ { compile(tests.At(i)) } } else { // Запускаем "compile()" в параллельных потоках. fmt.Printf("- running %d concurrent job(s)\n", *jobs) // Канал задач: в этот канал мы будем класть имена файлов, // которые надо скомпилировать. Потоки-runner'ы будут ждать // сообщений из этого канала. Канал имеет ограничение по // длине. Это аналог семафора, чтобы блокировать главный // поток, если все runner'ы заняты. tasks := make(chan string, *jobs) // Канал подтверждения полного завершение потока-runner'а. // Главный поток будет ждать, пока все runner'ы ответят // по этому каналу. Тип сообщений тут не важен. done := make(chan bool) // Запускаем runner'ы. for i := 0; i < *jobs; i++ { go runner(tasks, done) } // Передаем в канал имена файлов для обработки. При // достижении максимального размера канала, главный поток // будет заблокирован. for i := 0; i < tests.Len(); i++ { tasks <- tests.At(i) } // Посылаем всем потокам команду завершиться и ждем // подтверждения о нормальном выходе от каждого потока. for i := 0; i < *jobs; i++ { tasks <- "" <- done } } } // Поток-runner. func runner(tasks chan string, done chan bool) { // Бесконечный цикл. for { // Ждем сообщения из канала. Обычно, поток заблокирован // на этом месте. name := <- tasks // Если имя пустое, нас просят завершиться. if len(name) == 0 { break } // Компилируем файл. compile(name) } // Посылаем сообщение, что поток завершился. done <- true } func compile(name string) { // Вызываем компилятор. c, err := exec.Run(*compiler, []string{*compiler, name}, os.Environ(), "./data", exec.DevNull, exec.PassThrough, exec.PassThrough) if err != nil { fmt.Printf("unable to compile %s (%s)\n", name, err.String()) os.Exit(1) } c.Wait(0) }
Makefile:
target = tar_extractor all: 6g $(target).go 6l -o $(target) $(target).6
Я погонял это добро под Линуксом 64-бит на восьми процессорном блейде. Во время тестирования я был на машине один, так что результаты разных прогонов можно сравнивать. Файл “huge.tar” содержит ~15 тысяч исходников и имеет размер один гигабайт.
Так выглядит загрузка процессоров, когда машина ничего не делает (все процессоры почти на 100% в idle):
Cpu0 : 0.0%us, 0.0%sy, 0.0%ni,100.0%id, 0.0%wa, 0.0%hi, 0.0%si, 0.0%st
Cpu1 : 0.0%us, 0.0%sy, 0.0%ni, 99.7%id, 0.3%wa, 0.0%hi, 0.0%si, 0.0%st
Cpu2 : 0.0%us, 0.0%sy, 0.0%ni,100.0%id, 0.0%wa, 0.0%hi, 0.0%si, 0.0%st
Cpu3 : 0.0%us, 0.0%sy, 0.0%ni,100.0%id, 0.0%wa, 0.0%hi, 0.0%si, 0.0%st
Cpu4 : 0.0%us, 0.0%sy, 0.0%ni,100.0%id, 0.0%wa, 0.0%hi, 0.0%si, 0.0%st
Cpu5 : 0.0%us, 0.3%sy, 0.0%ni, 99.3%id, 0.3%wa, 0.0%hi, 0.0%si, 0.0%st
Cpu6 : 0.0%us, 0.0%sy, 0.0%ni,100.0%id, 0.0%wa, 0.0%hi, 0.0%si, 0.0%st
Cpu7 : 0.0%us, 0.0%sy, 0.0%ni,100.0%id, 0.0%wa, 0.0%hi, 0.0%si, 0.0%st
Запускаем в последовательном режиме (-jobs 0
):
make && time -p ./tar_extractor -jobs 0 huge.tar
Время работы:
real 213.81
user 187.32
sys 61.33
Практически все процессоры на 70-80% ничего не делают (все снимки я делал во время стадии компиляции):
Cpu0 : 11.9%us, 4.3%sy, 0.0%ni, 82.5%id, 1.3%wa, 0.0%hi, 0.0%si, 0.0%st
Cpu1 : 9.6%us, 2.7%sy, 0.0%ni, 87.7%id, 0.0%wa, 0.0%hi, 0.0%si, 0.0%st
Cpu2 : 4.3%us, 1.3%sy, 0.0%ni, 92.7%id, 1.7%wa, 0.0%hi, 0.0%si, 0.0%st
Cpu3 : 16.0%us, 6.0%sy, 0.0%ni, 78.0%id, 0.0%wa, 0.0%hi, 0.0%si, 0.0%st
Cpu4 : 12.6%us, 4.3%sy, 0.0%ni, 82.7%id, 0.3%wa, 0.0%hi, 0.0%si, 0.0%st
Cpu5 : 11.6%us, 3.3%sy, 0.0%ni, 85.0%id, 0.0%wa, 0.0%hi, 0.0%si, 0.0%st
Cpu6 : 4.7%us, 1.3%sy, 0.0%ni, 94.0%id, 0.0%wa, 0.0%hi, 0.0%si, 0.0%st
Cpu7 : 16.6%us, 6.3%sy, 0.0%ni, 77.1%id, 0.0%wa, 0.0%hi, 0.0%si, 0.0%st
Суммарная загрузка процессоров - 2.7%:
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
15054 tester 18 0 41420 4980 1068 S 2.7 0.1 0:02.96 tar_extractor
Теперь запускаем с пулом потоков, но только с одним каналом (-jobs 1
):
Время:
real 217.87
user 191.42
sys 62.53
Процессоры:
Cpu0 : 5.7%us, 1.7%sy, 0.0%ni, 92.7%id, 0.0%wa, 0.0%hi, 0.0%si, 0.0%st
Cpu1 : 13.3%us, 5.3%sy, 0.0%ni, 81.3%id, 0.0%wa, 0.0%hi, 0.0%si, 0.0%st
Cpu2 : 7.0%us, 2.7%sy, 0.0%ni, 89.3%id, 0.7%wa, 0.0%hi, 0.3%si, 0.0%st
Cpu3 : 15.3%us, 5.7%sy, 0.0%ni, 77.7%id, 1.3%wa, 0.0%hi, 0.0%si, 0.0%st
Cpu4 : 6.0%us, 2.0%sy, 0.0%ni, 92.0%id, 0.0%wa, 0.0%hi, 0.0%si, 0.0%st
Cpu5 : 14.3%us, 7.3%sy, 0.0%ni, 78.4%id, 0.0%wa, 0.0%hi, 0.0%si, 0.0%st
Cpu6 : 7.0%us, 2.3%sy, 0.0%ni, 90.7%id, 0.0%wa, 0.0%hi, 0.0%si, 0.0%st
Cpu7 : 15.3%us, 6.6%sy, 0.0%ni, 78.1%id, 0.0%wa, 0.0%hi, 0.0%si, 0.0%st
Понятно, что картина такая же, так как реально мы также гоняем один поток.
А теперь включаем пул потоков (-jobs 32
):
make && time -p ./tar_extractor -jobs 32 huge.tar
Время работы упало почти в семь раз:
real 38.38
user 195.55
sys 69.92
Общая загрузка процессоров (во время стадии компиляции) возросла до 23%:
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
17488 tester 16 0 45900 9732 1076 S 23.6 0.1 0:06.40 tar_extractor
Видно, что все процессоры реально заняты:
Cpu0 : 56.3%us, 26.3%sy, 0.0%ni, 17.3%id, 0.0%wa, 0.0%hi, 0.0%si, 0.0%st
Cpu1 : 55.5%us, 27.9%sy, 0.0%ni, 15.6%id, 1.0%wa, 0.0%hi, 0.0%si, 0.0%st
Cpu2 : 56.1%us, 25.9%sy, 0.0%ni, 15.0%id, 0.7%wa, 0.3%hi, 2.0%si, 0.0%st
Cpu3 : 58.1%us, 26.2%sy, 0.0%ni, 15.6%id, 0.0%wa, 0.0%hi, 0.0%si, 0.0%st
Cpu4 : 57.2%us, 25.8%sy, 0.0%ni, 17.1%id, 0.0%wa, 0.0%hi, 0.0%si, 0.0%st
Cpu5 : 56.8%us, 26.2%sy, 0.0%ni, 16.9%id, 0.0%wa, 0.0%hi, 0.0%si, 0.0%st
Cpu6 : 59.0%us, 26.3%sy, 0.0%ni, 13.0%id, 1.7%wa, 0.0%hi, 0.0%si, 0.0%st
Cpu7 : 56.5%us, 27.2%sy, 0.0%ni, 16.3%id, 0.0%wa, 0.0%hi, 0.0%si, 0.0%st
Данное тестирование предназначено исключительно для понимания, как простейший параллельный код ускоряет все в разы. И также для демонстрации, как просто и относительно безопасно можно программировать параллельные вычисления в Go.
Посты по теме Go:
]]>С багом мы разобрались (включая проблему в Makefile), но возник у меня общий вопрос: с каким именно кодом завершается процесс, если он упал, не успев выполнить exit().
В UNIXах есть специальные макросы, которыми можно проинспектировать код возврата wait(). Но, все UNIXы разные, и к тому же есть еще Windows.
В итоге я написал небольшую самопадающую программу:
#include <stdio.h> #include <stdlib.h> #include <string.h> int main(int argc, char* argv[]) { char cmd[80]; int r; sprintf(cmd, "%s ?", argv[0]); if (argc > 1) { if (argv[1][strlen(argv[1]) - 1] == '1') *(char *)0 = 0; exit(0x77); } printf("Normal: %08X\n", system(cmd)); cmd[strlen(cmd) - 1] = '1'; printf("Crash : %08X\n", system(cmd)); return 0; }
И запустил на некоторых характерных машинах.
Windows 7, Visual Studio 2010, cl crash.c && crash
:
Normal: 00000077
Crash : C0000005
Linux x86_64 (cс -o crash crash.c && ./crash
):
Normal: 00007700
Crash : 0000000B
Сигнал 0x0B
(13) - это, кстати, SIGSEGV
, segmentation violation, что, собственно, и произошло.
Solaris SPARC 5.10:
Normal: 00007700
Segmentation Fault - core dumped
Crash : 00008B00
HP-UX Itanium 2:
Normal: 00007700 sh: 25112 Memory fault(coredump) Crash : 00008B00
AIX 5.2
Normal: 00007700 Crash : FFFFFFFF
Тут, видимо, до system()
код возврата не дошел совсем.
Вывод: все крайне зависит (как всегда) от операционной системы.
]]>В итоге я занимаюсь тем, что исправляю то там, то тут эти частные случаи. И обычно меняешь одно, и ломаешь десяток других мест.
В итоге после пары недель мытарств, я таки взял cmockery, написал всю необходимую «обвеску» и переделал все примеры в тесты.
Например:
void test_Ticket_dd6a19efa5_DATE(void **state) { string_eq("0029 ENTRIES<1, AB.CDE.VALUE.DATE> = TODAY", "..............a........................b......"); string_eq("0036 ENTRIES<1, AB.CDE.BOOKING.DATE> = TODAY", "..............a........................b......"); string_eq("0036 ENTRIES<1, AB.CDE.TOOKING DATE> = TODAY", "..............a.................bbbb...c......"); string_eq("0036 DATE = TODAY", ".....aaaa.b......"); string_eq("0036 DATE = TODAY", "......aaaa.b......"); } void test_Ticket_e8e02762a0_V_TIME(void **state) { string_eq("0017 V.TIME = 'x'", "................a.bbb"); string_eq("0034 V.DELTA = TIME() - TIME1", ".................a.bbbb...c......"); } void test_Ticket_0bcfac1fb6_READNEXT_FROM(void **state) { string_eq("0167 READNEXT ID FROM 9 ELSE DONE = 1 ", ".........aaaaaaaa....bbbb.c.dddd......e.f."); }
И таких тестов сотни.
Макрос string_eq
не является стандартным для cmockery, и под ним скрывается приличный кусок моего велосипеда. Вызывается функция подсветки строки, и по ней делается проход для отмечания факта смены цвета путем увеличения индекса (начальный индекс цвета - а
). Точка значит неподсвеченный символ. Немного топорно, но позволяет не хардкодить в тестах коды конретных цветов. Конечно, сильно облегчает жизнь тот факт, что данный язык строчно-ориентированный. Иначе все было бы сложнее.
После этого жизнь радикально изменилась. Теперь я легко меняю код и одной командой проверяю, не сломал ли я чего из старого. Те два дня, что я потратил на написание дополнительного кода для тестов уже в сотни раз окупились.
Каждую новую фичу (=очередной частный случай) или багфикс я начинаю с теста. И только потом код. Реально не могу представить, как бы я дальше работал над проектом без тестов.
Тут получается вообще чистая класска TDD – сначала тесты, а только потом код.
]]>Как выбрать мэра?
]]>С тех пор библиотека практически не изменилась. Она устраивала меня, а сторонних пользователей у нее особо не было.
Недавно Alexei Bezborodov вдохнул в проект новую жизнь, исправив несколько ошибок и, самое главное, выпустив новую версию.
Его ветка теперь является основной, а старая оставлена для истории.
Ниже анонс от Алексея.
Напишу здесь последние новости для интересующихся.
Появилась новая версия 0.1.0.
В ней добавлено:
LuaScript::Double_LuaArg
LuaScript::Vector_LuaArg
Рекомендую посмотреть тесты. В них есть пример работы с двухмерным вектором и другие полезности.
Стиль кода немного изменился. Для тех кто хочет работать со старым стилем, выложил версию 0.0.2. В ней исправлены некоторые мелкие недочеты, но новые возможности отсутствуют.
Информация по теме:
]]>Все перечисленное должно работать без исключения на Windows 2003⁄2008, Linux AS5+, AIX 5⁄6, Solaris (Intel/AMD), HP-UX v3.
Для всего остального мы прекрасно довольствуемся STL.
Компиляторы только “родные” для каждой платформы (то есть, например, Cygwin не годится для Windows, и gcc только на Linux).
Мы давного используем ACE, как основную библиотеку. В ней есть все из выше сказанного, даже имитация fork под Windows, когда идентификатор сокета передается в символьном виде дочернему процессу через командную строку.
Вообще, fork()
- это настоящая засада под Windows. Для его полноценной реализации нужно использовать недокументированные функции и структуры, как это делается в Cygwin, что, конечно, для нас неприемлемо в плане последующей поддержки. Что еще усугбляет ситуацию - нет возможности отказаться от мульти-процессной модели и просто перейти на потоки (поэтому и нужен fork).
Но ACE - это монстр, который еще и труден в правильной сборке на AIX и HP-UX. В нашем случае - это как стрелять из пушки по воробьям.
У нас есть желание пересеть на другую библиотеку. Например, boost. Но это тоже монстр, к тому же монстр, требовательный к свежести компиляторов в плане С++, но мы вынуждены “сидеть” порой на старых компиляторах, так как сотни клиентов их используют. Так что вопрос простоты сборки boost’а тоже под большим вопросом.
Есть и еще вариант - написать все самим на основе системных вызовов, openssl и pthreads. Здесь начинается проблема сопровождения и тщательного тестирования на каждой платформе. С другой стороных - нет зависимости от стороннего почти ненужного монстра (ACE или boost), легкость и прозрачность исходников.
Можно также подобный доморощенный framework выложнить в open-source, и может быть привлечь тем самым сторонних людей для полировки библиотеки.
Хотел бы совета на тему, какую бы такую легковесную библиотеку выбрать, которая бы делала что описано выше, и при этом не была монструозной.
]]>Как вы думаете, что обычно происходит сразу после знаменательной даты выплаты бонусов? Правильно - начинается массовое катапультирование людей (другими словами, те, кто планировал уволится - пишут заявление). И забавно наблюдать, как десятки людей уходят практически в один день.
Мораль: готовые ожидаемые бонусы - это дестабилизирующее зло. Плати людям достойную зарплату регулярно и выплачивай бонусы по итогам сделанной работы сразу, чтобы для их получeния надо было работать, а не просиживать штаны в их ожидании.
В декабре я ушел из Блумберга и с начала года вернулся в свою предыдущую компанию Теменос.
Я не стал ждать златоносного бонусного марта. Мне сделали отличное предложение на менеджерскую позицию, от которого было невозможно отказаться.
А давеча вчера я был на коллективной пьянке нашего солидарного программисткого брата, в общем и целом просвященной пачке увольненцев из Блумберга. Сейчас реальная движуха на рынке - инвестиционные банки берут людей пачками, предлагая очень хорошие зарплаты.
В целом, в ротации людей нет особо ничего плохого. Те, кто уходят, довольны, что нашли что-то лучше. Те, кто остается, тоже не должны особо страдать, так как появляются места в компании, которые можно будет заполнить. Еще это значит, что в компании начинается новый виток найма.
]]>Сегодня на ринге Студия 2010, lcc и tcc. Все три я активно использую под Windows для языка С.
Сравнивать будем на любимом решете Эратосфена (erato-c-int.c
):
#include <stdio.h> #include <stdlib.h> #include <memory.h> #include <string.h> #include <math.h> int main(int argc, char* argv[]) { int n = argc > 1 ? atoi(argv[1]) : 100; int* S; int count; int sz = n * sizeof(*S); int i, j; long sqrt_n = sqrt(n) + 1; printf("%d\n", n); S = malloc(sz); memset(S, 0, sz); for (i = 2; i < sqrt_n; ++i) if (S[i] == 0) for (j = i*i; j < n; j+=i) S[j] = 1; count = 0; for (i = 2; i < n; ++i) if (S[i] == 0) count++; printf("%d\n", count); free(S); return 0; }
Скрипт для запуска (Makefile):
SRC=erato-c-int N=100000000 all: build run build: build-cl build-lcc build-tcc run: run-cl run-lcc run-tcc build-cl: @cl /nologo /O2 -Fe$(SRC)-cl.exe $(SRC).c run-cl: @echo --- -@cl 2>&1 | findstr Compiler @ntimer.exe $(SRC)-cl.exe $(N) | findstr ETime build-lcc: @c:\lcc\bin\lcc -O2 -o $(SRC)-lcc.obj $(SRC).c @c:\lcc\bin\lcclnk -o $(SRC)-lcc.exe $(SRC)-lcc.obj run-lcc: @echo --- @c:\lcc\bin\lcc -v @ntimer.exe $(SRC)-lcc.exe $(N) | findstr ETime build-tcc: @c:\tcc\tcc -O2 -o $(SRC)-tcc.exe $(SRC).c run-tcc: @echo --- @c:\tcc\tcc -v @ntimer.exe $(SRC)-tcc.exe $(N) | findstr ETime
Погнали:
---
Microsoft (R) 32-bit C/C++ Optimizing Compiler Version 16.00.30319.01 for 80x86
ETime( 0:00:04.374 ) UTime( 0:00:04.196 ) KTime( 0:00:00.124 )
---
Logiciels/Informatique lcc-win32 version 3.8. Compilation date: Jan 29 2011 11:51:05
ETime( 0:00:04.415 ) UTime( 0:00:04.102 ) KTime( 0:00:00.202 )
---
tcc version 0.9.25
ETime( 0:00:04.944 ) UTime( 0:00:04.492 ) KTime( 0:00:00.280 )
Вывод не особо захватывающий. Все выступили примерно одинаково.
Посты по теме:
]]>Когда-то я немного увлекался микроэлектроникой, и в частности, программированием микроконтроллеров типа PICmicro. Мои познания в мире электронных схем, особенно аналоговых, крайне скромны и не уходят дальше институтского курса электротехники. Хотя в цифровой электронике немного проще, ибо там многое уже идет из понятного двоичного мира.
Когда возишься с микроконтроллерами так или иначе приходится брать в руки паяльник. Я это не очень люблю, хотя даже освоил весьма забавный способ нанесения разводки на печатную плату с помощью лазерного принтера и утюга для последующего травление хлорным железом. Мне сразу хочется перейдти к возьне с прошивкой, нежели с пайкой. Благо, современные средства макетирования типа того, что на картинке ниже, сильно упрощают задачу.
Подобные макетные платы меня весьма устраивали.
Но сегодня я познал совершенно иной уровень - абсолютно цифровой.
Брат дал возможность повозиться с софтом для эмулирования электрических схем. Называется, Proteus ISIS.
Тут просто какой-то беспредел. Ты просто рисуешь схему, набирая ее из огромной базы компонент, а потом ее просто запускаешь! В реальном времени! И этот софт делает имитацию схемы на уровне законов физики в плане электричества. Я не знаю, какова “глубина” имитации (врядли на уровне электронов ;-), но выглядит, очень убедительно (см. мои примеры ниже).
Конечно, первым делом мы нарисовали классический мульти-вибратор.
На видео видно, как происходит симуляция (рекомендую полный экран). Обратите внимание на поведение светодиода, вольтметров и амперметров, и, самое прикольное, пластин конденсаторов! Они показывают, как и когда происходит их заряд-разряд.
А теперь подключаем осциллограф! Конечно тоже цифровой.
Тут тебе и фронты сигналов, задержки, развертки и все такое.
Я вообще человек не очень впечатлительный в плане софта, но тут у меня была минута молчания, пока я осознавал масштабы возможностей. Ты фактически создаешь аналоговую схему, но макетируешь ее не то, чтобы без паяльника, я просто водя мышкой по экрану.
Я не подозревал, что все ушло так далеко вперед.
Дальше, больше. Резисторы, емкости, транзисторы и т.д. - это мелочи. Данный софт позволяет вставить в схему микроконтроллер! Например, PICmicro, загрузить в него прошивку, подать виртуальные вольты/амперы питания и поглядеть, как это все будет шевелиться в реальном времени!
Среди множества стандартных примеров в дистрибутиве есть проект - музыкальный звонок.
Я открыл проект и запустил.
Например, меняя номинал резистора, можно видеть, как меняется свечение связанного с ним светодиода. Также на видео ниже видно работу эмулятора микроконтроллера.
Ну и под занавес (пристегните ремни!). Допустим, вы разрабатываете на микроконтроллере устройство, которое будет подключаться к компьютеру по USB. Вам надо собрать реальный макет, залить в него прошивку, позаботиться о драйвере и уже только после этого подключать и смотреть, работает или нет (с первого раза, скорее всего нет).
В этом софте можно сделать проект (схему, прошивку) и виртуально подключить в реальную подсистему USB Windows! И попробовать ваше устройство в деле после двух-трех кликов мышкой.
В общем, господа, лично я полном шоке.
Дистрибутив весит всего около 70 мегов.
P.S. Софт коммерческий, поэтому не спрашивайте меня, где его брать.
P.S. Видеокаст записывал первый раз в жизни, поэтому прошу прощения за некоторый сумбур.
]]>И все бы ничего, когда клиентская машина работает на Windows. В этом случае ты просто заходишь на страничку, логин/пароль, прямо со страницы стартует activex, который ставит и запускает все что нужно, и можно работать.
Сложности начинаются, когда клиентская машина - это не Windows. Конечно, клиенты RDP уже есть не только под Windows, но это не особо помогает, так как без того особого софта для туннеля ничего работать все равно не будет. И как назло, нет возможности докупить серверную часть этого VPN’а для поддержки клиентов не только под Windows.
Что люди, пользователи Linux и Mac, вынуждены делать? Ставить Windows на виртуальной машине и запускать RDP в ней. Хорошее решение, но с одним недостатком. Виртуальная машина с обычными виндами расползается на пару гигов как минимум, и загружается не очень быстро.
Я немного погуглил, и наткнулся на MicroXP. Это сильно урезанная версия XP SP3. Дистрибутив занимает ~100 мегов, а в установленном виде ~250. Под VirtualBox’ом на Mac Air установка занимает минут пять, а уже установленная система статует секунд за 10. Виртуальная машина минимальна - динамический диск на 300 мегов (реально будет ~250) и 64 мегов памяти.
После установки MicroXP надо доустановить “Virtual Box Guest Additions” (~200кб) для возможности расшаривать каталоги и не мучаться с мышкой, и добавить клиента RDP (его, как раз, можно перенести через расшареный каталог).
В общем, если вам нужны минималистические ультрабыстрые XP, то MicroXP отличный кандидат.
P.S. Вопросы легальности этой сборки XP оставим на бортом. Поэтому ссылки ведут на торренты. Кстати, не ходите на сайт microxp.org. Это fake, для сбора почтовых адресов, видимо.
]]>Замечание: все цитируемые ниже фрагменты приводятся как есть, с минимальными правками в плане форматирования на странице.
В реальности все было иначе. Буквально через пару часов после поста на Хабре пришло письмо от Vasiliy Artemev:
Secret code: 139471
Это было правильным ответом. Василий официально стал первым и получил законные 100$ призовых. Он любезно рассказал, что взломал методом грубой силы, перебором. Действительно, простейшая переборная атака заставляла программу выдать секретное слово уже на 3-х символьном пароле. Увы, моя реализация хеш-функции для пароля стала уязвимым звеном.
Но полного анализа не было, и к тому же Василий предложил спонсировать дальнейший анализ задачи в виде 50$ из начального призового фонда. В общем, анализ продолжался.
Тем временем я сделал новую версию задачи, где уже не было хеширования, а был просто зашифрованный пароль. Я думал, это усложнит взлом. Но вечером того же дня приходит письмо от Anton Bukov:
Ответ на NORCPU hackme challenge, Version 2: “R0und2 D0ne!” ?
Это правильный ответ. Я недоумевал. Как можно было так быстро разобраться?
Антон также любезно рассказал, как ему удалось получить ответ:
Мой взлом основан на строке:
mem = mem_0.slice(0);
Тут массив копируется, если дважды вызвать функцию calc на одном массиве, то для любого пароля будет отображен ответ. У меня в коде это выглядело так:
wcout << calc(L"0") << endl << calc(L"0") << endl;
Я так догадываюсь для предотвращения этого и осуществлялось копирование. Боюсь это не тот способ, которого вы ждали. Я и сам удивился, когда заметил ответ в output-е. Ну а после уже разобрался из-за чего он вылез.
Антон случайно нашел баг, из-за которого программа опять сама выдавала секрет. Надо было запустить программу повторно без приведения памяти эмулятора в исходное состояния.
Корень проблемы:
... IS_0(flag) JZ okay EXIT(1) okay: ; print the secret ...
После первого неправильного прогона и отказа в выводе секрета программа останавливалась в строке 4, и регистр комманд ip
уже указывал на метку okay
. Поэтому если интерпретатор просто запустить еще раз без инициализации памяти (а все регистры, включая ip
находятся в памяти), он продолжит выполнение с метки okay
и выведет секрет.
Досадно. Я быстро исправил проблему и выложил версию 2.1, в котором этого эффекта уже не было.
Прошло пару дней.
И вот приходит письмо от пользователя a5b с Хабра:
Хотпатчинг переходов и ответ pw:
abcd
resp:R0und2 D0ne!
- пароль естественно неверный
- инвертировал записываемые данные в
(i == 27692 || i == 31712)
Формально, это решение, но нет пароля, а значит полного анализа тоже нет.
Но через час от a5b приходит довесок:
h1cKmE1fUsAn input data: chr conI LEET xor CHR1 13417 13313 104 h CHR2 39953 39968 49 1 CHR3 54302 54397 99 c CHR4 32223 32148 75 K CHR5 30900 30937 109 m CHR6 27373 27304 69 E CHR7 16420 16405 49 1 CHR8 49210 49244 102 f CHR9 16740 16689 85 U CHR10 50115 50096 115 s CHR11 19308 19245 65 A CHR12 57802 57764 110 n static init: CHRi 59609= 59651 CONi 59610= 59634 CNTR 59611=12 SUM 59608=0 LEET 59607 = 13313 1: [59609]+1 -> [59609] // select next chr [59610]+1 -> [59610] // select next con SUM |= CHRi ^ LEET ^ CONi LEET = LEET * 3 + 29 [59611]: if([59611] != 0) Loop 1 ... if(SUM != 0) exit else print R0und2 D0ne // эту часть подробно не смотрел. // Судя по модификациям кода (продвижение индекса), загружает 12 констант: // 29528 22899 2971 9089 27542 17353 52278 25635 11626 34909 39131 51838, // над каждой колдует и пишет в вывод
А вот это уже анализ. a5b стал первым, кто прислал алгоритм задачи номер 2.
В тот же день вечером приходит письмо от Max Filippov:
Algorithm that was used to check password correctness in the first round was the following:
bool check(const char *p) { int v = 0x1040; for(; *p; ++p) { v ^= *p; for (int i = 0; i < 8; ++i) { int f = v & 1; v >>= 1; if (f) v ^= 0x1408; } } return v == 0x1c89; }
that is, sort of CRC.
To discover it I’ve collected NORCPU execution trace and “disassembled” it.
Modified NORCPU source and disassembler are attached, and also may be found there: http://jcmvbkbc.spb.ru/git/?p=dumb/norcpu.git;a=summary
И довесок:
The method used is pretty straightforward:
- collect execution trace
- recognize instruction patterns and collapse sequences of primitive instructions to more complex ones
- analyze disassembled trace
So, first I needed trace: I copied javascript text into cpp source, fixed lingual differences and inserted the following printf:
while (1) { int i = mem[ip]; printf("%04x:NOR(%04x, %04x => %04x) ", i, mem[i], mem[i + 1], mem[i + 2]); int a = mem[i + 0];
so that I got a long line (about 8Mb) of primitive instruction execution trace.
Then I started constructing
sed
script that would make it readable.First, it broke the trace line-wise, one instruction per line (288323 lines, will read it in case of insomnia). I took a look at processed trace and recorded several obvious instruction patterns into
sed
. Then reran script, took next look, recorded more patterns, …This way I figured out all boolean logic commands and jumps. Then rotations left. Each time new command got recognized, new filtered processed trace was suggesting next step, e.g. 15 ROTL equals ROTR etc.
Then I looked into your article at “Модель процессора с одной командой”. And found addition pattern in disassembly. And recorded it in
sed
script.After that I was able to just read the trace (which shrunk to 1035 lines). Its inner loop fit into one page, I just made some notes on a scratchpad:
[f1ba]: current in-T index (i) [f1b4]: LEN [f1b5]: 8 0012-0035:[f1b9] ^= (T[i] & 0xff) 006d-007b:[f1b8] = [f1b9] & 1 008a-0158:[f1b9] >>= 1 0167-10c7:[f1aa] = [f1b8] + -1, [f1ab] = !carry 10ca-10e6:jmp on carry to 1145:110d 110d-111b:[f1b9] ^= 1408 1145-1f4f:--[f1b5] 20a5-3005:[f1aa] = [f1b5] + -1, f1ab = !carry 3008-3024:jmp on carry to 006d:304b 304b-304b:++i 3fab-3fab:--LEN 4f0b-5e8a:jmp on carry to 5eb1:6
then I browsed through the repetitions of this inner loop and found the end of the outer loop.
5eb1-6e74:check 1c89
Then just translated it into C. It all took me three evenings.
Затем от Max Filippov пришло решение и второй задачи.
Ответ на второй тур –
h1cKmE1fUsAn
Результат –
R0und2 D0ne!
Алгоритм проверки пароля такой:
bool check(const char *p) { static const int xor_array[] = { 0x3469, 0x9c11, 0xd41e, 0x7ddf, 0x78b4, 0x6aed, 0x4024, 0xc03a, 0x4164, 0xc3c3, 0x4b6c, 0xe1ca, }; int v = 0; int x = 0x3401; for (int i = 0; i < 12; ++i) { int f = p[i] ^ x ^ xor_array[i]; // printf("x: %04x, f: %04x\n", x, f); v |= f; x = (x * 3 + 0x1d) & 0xffff; } return !v; }
Закомментированный
printf
выводит ключевую фразу по ходу.Методика анализа – как и в первом туре – дизассемблирование трассы выполнения.
Первый тур был откровенно интереснее.
И, наконец, последнее полученное решение от Salo Kril для первой задачи.
Особых пояснений нет – просто исходники.
// Генерация паролей // Brute_force(3); WORD ks_f(char *buff, int len) { int i, j; WORD ks = 0x1040; for (i = 0; i < len; i++) { ks ^= buff[i]; for (j = 0; j < 8; j++) { if((ks & 1) == 0) ks = ks >> 1; else ks = (ks >> 1) ^ 0x1408; } } return ks; } void Brute_force(int n) { int i; static char alphabet[] = "\x20\x21\x22\x23\x24\x25\x26\x27\x28\x29\x2A\x2B\x2C\x2D\x2E\x2F" "\x30\x31\x32\x33\x34\x35\x36\x37\x38\x39\x3A\x3B\x3C\x3D\x3E\x3F" "\x40\x41\x42\x43\x44\x45\x46\x47\x48\x49\x4A\x4B\x4C\x4D\x4E\x4F" "\x50\x51\x52\x53\x54\x55\x56\x57\x58\x59\x5A\x5B\x5C\x5D\x5E\x5F" "\x60\x61\x62\x63\x64\x65\x66\x67\x68\x69\x6A\x6B\x6C\x6D\x6E\x6F" "\x70\x71\x72\x73\x74\x75\x76\x77\x78\x79\x7A\x7B\x7C\x7D\x7E"; if(n == 0) { if(ks_f(buff_bf, BF_N) == 0x1c89) printf("%s\n",buff_bf); return; } n--; for (i = 0; alphabet[i]; i++) { buff_bf[n] = alphabet[i]; Brute_force(n); } }
и реконструированный код:
#define DEST_COUNT 0xF1FE extern WORD mem[]; /* Secret code: 139471 */ void reconstructed_fn(void) { int i, j; WORD src, dest, key, count, hash, hash_OK, key_const; src = mem[0xF1BA]; // input string count = mem[0xF1ED]; // input string length dest = mem[0xF1BB]; // 0xf1ff hash_OK = mem[0xF1BC]; // 0x1c89 hash = mem[0xF1B9]; // 0x1040 for(i = 0; i < count; i++) { hash ^= mem[src + i] & 0xFF; for (j = 0; j < 8; j++) { if ((hash & 1) == 0) hash = hash >> 1; else hash = (hash >> 1) ^ 0x1408; } } if (hash == hash_OK) { src = mem[0x6EB6]; // "Secret code: 139471" count = mem[0xF1C7]; // 19 key = ((hash >> 8) ^ hash) + 1; key_const = mem[0x6EA8]; // 11 } else { src = mem[0x5EBE]; // "Wrong password!" count = mem[0xF1DC]; // 15 key = mem[0xF1BD]; key_const = mem[0xF1BE]; // 17 } mem[DEST_COUNT] = count; for (i = 0; i < count; i++) { mem[dest + i] = mem[src + i] ^ key; key = key * 3 + key_const; } } /* -------------------------------------------------------------------------------------------- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -------------------------------------------------------------------------------------------- */ WORD and(WORD w1, WORD w2) { return w1 & w2; } WORD or(WORD w1, WORD w2) { return w1 | w2; } WORD xor(WORD w1, WORD w2) { return w1 ^ w2; } WORD rol(WORD w1, WORD n) { return (w1 << n) | (w1 >> (16 - n)); } WORD ror(WORD w1, WORD n) { return (w1 >> n) | (w1 << (16 - n)); } WORD extend_16(WORD k1) { int i, k2; for (i = 0, k2 = 0; i < 16; i++) { k2 = or(k2, k1); k1 = rol(k1, 1); } return k2; } WORD add(WORD *kk1, WORD a, WORD b) { WORD mask_bit, aa0, aa1, tmp1, i, k1, k2; k1 = 0; k2 = 0; mask_bit = 1; for (i = 0; i < 16; i++) { k1 = and(k1, mask_bit); aa0 = xor(a, b); tmp1 = xor(aa0, k1); tmp1 = and(tmp1, mask_bit); k2 = or(tmp1, k2); aa1 = and(a, b); aa0 = and(k1, aa0); k1 = or(aa1, aa0); k1 = rol(k1, 1); mask_bit = rol(mask_bit, 1); } k1 = and(k1, mask_bit); *kk1 = extend_16(k1); return k2; } void reconstr(void) { WORD k1, k2, src, dest, tmp1, key, count, tmp2, tmp3, i, ks, ks_OK, key_a; WORD mask_bit, aa1, aa0; k1 = mem[0xF1AF]; // 0x88 k2 = mem[0xF1A3]; // 0x47 src = mem[0xF1BA]; // input string count = mem[0xF1ED]; // input string length dest = mem[0xF1BB]; // 0xf1ff ks_OK = mem[0xF1BC]; // 0x1c89 "w":ks=0x0ACC "WWW":0x0FCE "123456789":ks=0x05E3 ks = mem[0xF1B9]; // 0x1040 l_0006: ks = xor(ks, and(mem[src], 0xFF)); i = 8; l_006D: tmp3 = and(ks, 1); ks = ror(ks, 1); ks = and(ks, 0x7FFF); tmp2 = add(&k2, tmp3, -1); if(k2 != 0) { ks = xor(ks, 0x1408); } l_1145: i = add(&k1, i, -1); tmp2 = add(&k2, i, -1); if(k2 != 0) goto l_006D; l_304B: src = add(&k1, src, 1); count = add(&k1, count, -1); tmp2 = add(&k2, count, -1); if(k2 != 0) goto l_0006; src = mem[0x5EBE]; // Wrong password! count = mem[0xF1DC]; // 15 key = mem[0xF1BD]; key_a = mem[0xF1BE]; ks_OK = xor(ks_OK, ks); tmp2 = add(&k2, ks_OK, -1); if(k2 != 0) goto l_8552; l_6E9B: src = mem[0x6EB6]; // Secret code: 139471 count = mem[0xF1C7]; // 19 key = ks; key_a = mem[0x6EA8]; key = ror(key, 8); key = and(key, 0xFF); key = xor(ks, key); key = and(key, 0x7FFF); key = add(&k1, key, 1); l_8552: mem[DEST_COUNT] = count; l_8558: mem[dest] = xor(mem[src], key); tmp3 = add(&k1, key, key); key = add(&k1, key, tmp3); key = add(&k1, key, key_a); src = add(&k1, src, 1); dest = add(&k1, dest, 1); count = add(&k1, count, -1); tmp2 = add(&k2, count, -1); if(k2 != 0) goto l_8558; l_F186: mem[0xF1B2] = mem[0xF193]; // nc }
Итак, как вы уже поняли, решения были полностью исчерпывающими.
Всем приславшим огромное спасибо за проявленное внимание.
Ниже привожу оригинальные исходники обоих задач. Надо просто запустить питоновский скрипт, он скомпилирует код, написанный через функции-макросы, сделает тестовый прогон и сгенерирует html-страничку (нужен файл-шаблон template.html
).
Весь архив вместе c решениями-взломами доступны в виде git репозитория.
Файл norcpu.py (template.html):
import sys, re, time, string, binascii verbose = False verbose_cpu = False scramble = True test_wrong_crc = 0 secret_code = "Secret code: 139471" password = "h0cKmE1fUsAn" guess = "123456789012" guess = password # Secret code message encryption mask. secret_coef_add = 17 message_text = "Wrong password!" # Wrong password message encryption mask. message_mask = 0x6301 message_coef_add = 11 # Non-standard CRC initial value (should be 0xFFFF). crc16_initial_value = 0x1040 # Non-standard CRC constant (should be 0x8401). crc16_constant = 0x1408 code_segment = [] data_segment = [] label_count = 0 def dump(data, length = 8): result = [] for i in xrange(0, len(data), length): line = data[i:i + length] hex_line = ' '.join(["%04X" % x for x in line]) result.append("%04X: %-*s\n" % (i, length*5, hex_line)) return ''.join(result) def dump_js(data, length = 8): result = [] for i in xrange(0, len(data), length): line = data[i:i + length] hex_line = ' '.join(["0x%04X," % x for x in line]) result.append("%-*s\n" % (length*5, hex_line)) return ''.join(result) def calc_crc16(data): global crc16_initial_value global crc16_constant crc16 = crc16_initial_value for i in range(0, len(data)): ch = ord(data[i]) & 0xff crc16 = crc16 ^ ch for j in range(0, 8): if ((crc16 & 1) != 0): crc16 = (crc16 >> 1) ^ crc16_constant else: crc16 = crc16 >> 1 return crc16 crc16 = calc_crc16(password) def encode_string(data, name, mask, coef_add): global mem, names offset = names[name] offset_sz = names[name + "_sz"] for i in range(0, len(data)): mem[offset + i] = ord(data[i]) ^ mask mask = (mask * 3 + coef_add) & 0xffff mem[offset_sz] = len(data) def put_string(data, name): global mem, names offset = names[name] offset_sz = names[name + "_sz"] for i in range(0, len(data)): mem[offset + i] = ord(data[i]) mem[offset_sz] = len(data) def save_mem(name, size = -1): f = open(name, "w") if size == -1: size = len(mem) for i in (mem[0:size]): hex = "%04X" % i bin = binascii.a2b_hex(hex) f.write(bin) f.close() def next_label(): global label_count label_count = label_count + 1 return "label_%04d" % label_count def code_rem(comment): code_segment.append('; ' + comment) def data_rem(comment): data_segment.append('; ' + comment) def data_label(name): data_segment.append(name + ":") def code_label(name): code_segment.append(name + ":") def code(value): printed = value if type(value).__name__ == 'int': printed = "%d" % value code_segment.append(" dw %s" % printed) scramble_counter = 0x27 def next_scramble_counter(): global scramble_counter scramble_counter = scramble_counter * 3 + 7 return scramble_counter & 0xff def word(value): if value == -1: if scramble: value = next_scramble_counter() else: value = 0 printed = value if type(value).__name__ == 'int': printed = "%d" % value data_segment.append(" dw %s" % printed) def buffer(length, value = -1): for i in range(0, length): word(value) def var(name, value = -1): data_label(name); word(value); def NOR(a, b, r): code_rem('NOR ' + str(a) + ' ' + str(b) + ' ' + str(r)) code(a) code(b) code(r) def NOT(a, r): NOR(a, a, r); def OR(a, b, r): NOR(a, b, "or_reg") NOT("or_reg", r) var("or_reg") def AND(a, b, r): NOT(a, "and_reg_a") NOT(b, "and_reg_b") OR("and_reg_a", "and_reg_b", "and_reg_a") NOT("and_reg_a", r) var("and_reg_a") var("and_reg_b") def ANDi(a, imm, r): MOVi(imm, "and_i_reg") AND(a, "and_i_reg", r) var("and_i_reg") def XOR(a, b, r): NOT(a, "xor_reg_a") NOT(b, "xor_reg_b") AND(a, "xor_reg_b", "xor_reg_b") AND(b, "xor_reg_a", "xor_reg_a") OR("xor_reg_a", "xor_reg_b", r) var("xor_reg_a") var("xor_reg_b") def XORi(a, imm, r): MOVi(imm, "xor_i_reg") XOR(a, "xor_i_reg", r) var("xor_i_reg") def MOV(a, b): code_rem('MOV ' + str(a) + ' ' + str(b)) NOT(a, "move_reg") NOT("move_reg", b) code_rem('MOV END') var("move_reg") def JMP(a): code_rem('JMP ' + str(a)) MOV(a, "ip") def JMPi(a): code_rem('JMPi ' + str(a)) label = next_label() JMP(label) code_label(label) code(a) def MOVi(imm, a): code_rem('MOVi #' + str(imm) + ' ' + str(a)) label_data = next_label() label_jump = next_label() MOV(label_data, a) JMPi(label_jump) code_label(label_data) code(imm) code_label(label_jump) # [a] -> b def PEEK(a, b): label1 = next_label() label2 = next_label() MOV(a, label1) MOV(a, label2) code_label(label1) # NOT(0, 0, move_reg) code(0) # <- a code_label(label2) # code(0) # <- a code("move_reg") # NOT("move_reg", b) # a -> [b] def POKE(a, b): code_rem('POKE ' + str(a) + ' [' + str(b) + ']') label = next_label() MOV(b, label) NOT(a, "move_reg") # +3 (three operations) code("move_reg") # +4 code("move_reg") # +5 code_label(label) code(0) # <- b # imm -> [a] def POKEi(imm, a): MOVi(imm, "poke_i_reg") POKE("poke_i_reg", a) var("poke_i_reg") def EXIT(a): MOV(a, "exit_reg") def EXITi(a): MOVi(a, "exit_reg") def FADD(mask, carry, a, b, r): AND(a, mask, "fadd_reg_a") # zero bits in 'a' except mask'ed AND(b, mask, "fadd_reg_b") # zero bits in 'b' except mask'ed AND(carry, mask, carry) # zero bits in 'carry' except mask'ed # SUM = (a ^ b) ^ carry XOR(a, b, "fadd_reg_t1") XOR("fadd_reg_t1", carry, "fadd_reg_bit_r") # Leave only 'mask'ed bit in bit_r. AND("fadd_reg_bit_r", mask, "fadd_reg_bit_r") # Add current added bit to the result. OR("fadd_reg_bit_r", r, r) # CARRY = (a & b) | (carry & (a ^ b)) AND(a, b, "fadd_reg_t2") AND(carry, "fadd_reg_t1", "fadd_reg_t1") # CARRY is calculated, and 'shift_reg' contains the same value # but shifted the left by 1 bit. OR("fadd_reg_t2", "fadd_reg_t1", carry) # CARRY is shifted the left by 1 bit to be used on the next round. MOV("shift_reg", carry) # shift_reg = mask << 1 MOV(mask, mask) # mask = shift (effectively "mask = mask << 1") MOV("shift_reg", mask) AND(carry, mask, carry) var("fadd_reg_a") var("fadd_reg_b") var("fadd_reg_bit_r") var("fadd_reg_t1") var("fadd_reg_t2") def ZERO(a): XOR(a, a, a) def FADC(a, b, r): ZERO("fadc_reg_t") MOV("const_1", "fadc_reg_mask") for i in range(0, 16): FADD("fadc_reg_mask", "carry_reg", a, b, "fadc_reg_t") MOV("fadc_reg_t", r) ZERO("fadc_reg_t") for i in range(0, 16): OR("fadc_reg_t", "carry_reg", "fadc_reg_t") MOV("carry_reg", "carry_reg") MOV("shift_reg", "carry_reg") MOV("fadc_reg_t", "carry_reg") var("fadc_reg_mask") var("fadc_reg_t") def ADD(a, b, r): ZERO("carry_reg") FADC(a, b, r) def ADDi(a, imm, r): MOVi(imm, "add_i_reg") ADD(a, "add_i_reg", r) var("add_i_reg") def PUSH(a): ADD("stack_reg", "const_minus_1", "stack_reg") POKE(a, "stack_reg") def PUSHi(imm): MOVi(imm, "push_i_reg") PUSH("push_i_reg") var("push_i_reg") def POP(a): PEEK("stack_reg", a) ADD("stack_reg", "const_1", "stack_reg") def CALL(a): label = next_label() PUSHi(label) JMP(a) code_label(label) def CALLi(a): label = next_label() PUSHi(label) JMPi(a) code_label(label) def RET(): POP("ip") # Jump 'a', if cond = FFFF, and 'b' if conf = 0000 def BRANCH(a, b, cond): AND(a, cond, "branch_reg_a") # reg_a = a & cond NOT(cond, "branch_reg_b") # reg_b = !cond AND(b, "branch_reg_b", "branch_reg_b") # reg_b = b & reg_b = b & !cond OR("branch_reg_a", "branch_reg_b", "ip") # ip = (a & cond) | (b & !cond) var("branch_reg_a") var("branch_reg_b") # Jump 'a', if cond = FFFF, and 'b' if conf = 0000 def BRANCHi(a, b, cond): MOVi(a, "branch_i_reg_a") MOVi(b, "branch_i_reg_b") BRANCH("branch_i_reg_a", "branch_i_reg_b", cond) var("branch_i_reg_a") var("branch_i_reg_b") # if a != 0 -> carry = FFFF else carry = 0000 def IS_0(a): ZERO("carry_reg") FADC(a, "const_minus_1", "is_0_reg") NOT("carry_reg", "zero_reg") var("is_0_reg") var("zero_reg") # ip = (zero_reg == FFFF ? a : ip) def JZi(a): label = next_label() BRANCHi(a, label, "zero_reg") code_label(label) # ip = (zero_reg == FFFF ? a : ip) def JNZi(a): label = next_label() BRANCHi(label, a, "zero_reg") code_label(label) def ROL(a, b): MOV(a, a) # shift_reg = a << 1 MOV("shift_reg", b) def ROR(a, b): MOV(a, "ror_reg") for i in range(0, 15): ROL("ror_reg", "ror_reg") MOV("ror_reg", b) var("ror_reg") def SHL(a, b): ROL(a, b) ANDi(b, 0x0001, b) def SHR(a, b): ROR(a, b) ANDi(b, 0x7FFF, b) # NORCPU code var("ip", "start") var("shift_reg") var("carry_reg") var("const_1", 1) var("const_minus_1", 0xFFFF) var("exit_reg") var("stack_reg", "stack") code_label("start") var("i") var("j") var("ch") var("mask") var("t") var("crc16", crc16_initial_value) var("ptr", "password") MOV("password_sz", "i") crc_loop = next_label() code_label(crc_loop) # crc_loop # ch = *ptr PEEK("ptr", "ch") # ch &= 0xFF ANDi("ch", 0xFF, "ch") # crc16 ^= ch XOR("crc16", "ch", "crc16") MOVi(8, "j") crc_loop_j = next_label() code_label(crc_loop_j) # crc_loop_j # t = crc16 & 1 ANDi("crc16", 1, "t") # crc16 >>= 1 SHR("crc16", "crc16") IS_0("t") crc_loop_1 = next_label() JZi(crc_loop_1) # crc16 ^= crc16_constant XORi("crc16", crc16_constant, "crc16") code_label(crc_loop_1) # crc_loop_1 ADD("j", "const_minus_1", "j") IS_0("j") JNZi(crc_loop_j) # ptr += 1 ADD("ptr", "const_1", "ptr") # i = i - 1 ADD("i", "const_minus_1", "i") IS_0("i") JNZi(crc_loop) var("ptr2", "result") correct_crc = crc16 + test_wrong_crc var("correct_crc", correct_crc) # By default we're going to decrypt 'Wrong...' message. MOVi("message", "ptr") MOV("message_sz", "i") var("message_mask", message_mask) MOV("message_mask", "mask") var("coef_add", message_coef_add) wrong_label = next_label() XOR("correct_crc", "crc16", "correct_crc") IS_0("correct_crc") JNZi(wrong_label) # Now we switch to descrypt the secret message. MOVi(secret_coef_add, "coef_add") MOVi("secret", "ptr") MOV("secret_sz", "i") # mask = ((crc16 & 0xff) | ((crc16 >> 8) & 0xff)) + 1 MOV("crc16", "mask") for i in range(0, 8): SHR("mask", "mask") XOR("crc16", "mask", "mask") ANDi("mask", 0xff, "mask") ADD("mask", "const_1", "mask") code_label(wrong_label) MOV("i", "result_sz") loop = next_label() code_label(loop) # loop # ch = *ptr PEEK("ptr", "ch") # ch ^= mask XOR("ch", "mask", "ch") POKE("ch", "ptr2") # mask = mask * 3 + 11 ADD("mask", "mask", "t") ADD("mask", "t", "mask") ADD("mask", "coef_add", "mask") # ptr += 1 ADD("ptr", "const_1", "ptr") # ptr2 += 1 ADD("ptr2", "const_1", "ptr2") # i = i - 1 ADD("i", "const_minus_1", "i") IS_0("i") JNZi(loop) EXITi(0x00) buffer(8) data_label("stack") var("secret_sz", len(secret_code)) data_label("secret") buffer(len(secret_code) + 1) var("message_sz") data_label("message") buffer(16) var("password_sz") data_label("password") buffer(16) # The buffer holding the result string. var("result_sz") data_label("result") buffer(32) # Compiler text = code_segment text.extend(data_segment) if verbose: print "\n".join(text) # Phase 1. Calculate names. addr = 0 names = {} for line in text: if line[0] == ';': continue if line[0] != ' ': name = line.partition(':')[0] names[name] = addr else: addr = addr + 1 if verbose: print names raw_text = "\n".join(text) # Resolve names. for name in names: if verbose: print name, names[name], type(names[name]) name_re = re.compile(r'dw ' + name + '$', re.M) value = "%d" % names[name] raw_text = name_re.sub('dw ' + value, raw_text) text = raw_text.split("\n") if verbose: print "\n".join(text) # Phase 2. Compilation. addr = 0 comment = "" mem = [] for line in text: if line[0] == ';' or line[0] != ' ': comment = comment + line + ' ' else: value = int(line.strip().partition(" ")[2]) if verbose: print "%04X: %04X ; %s" % (addr, value, comment) mem.append(value) addr = addr + 1 comment = "" # Interpretation ip = names["ip"] exit_reg = names["exit_reg"] shift_reg = names["shift_reg"] carry_reg = names["carry_reg"] def nor(a, b): r = a | b r = r ^ 0xFFFF return r & 0xFFFF def norcpu(): while 1: i = mem[ip]; a = mem[i + 0] b = mem[i + 1] r = mem[i + 2] mem[ip] = i + 3 f = nor(mem[a], mem[b]) mem[r] = f mem[shift_reg] = ((f >> 15) & 1) | ((f & 0x7FFF) << 1) if verbose_cpu: print "%04X: %04X [%04X] %04X [%04X] -> %04X [%04X]" % \ (i, a, mem[a], b, mem[b], r, mem[r]) if r == exit_reg: break print "Starting from [%04X]" % mem[ip] # Encrypt the secret code. secret_mask = ((crc16 & 0xff) ^ ((crc16 >> 8) & 0xff)) + 1 encode_string(secret_code, "secret", secret_mask, secret_coef_add); # Encrypt 'Wrong...' message. encode_string(message_text, "message", message_mask, message_coef_add); mem_js = dump_js(mem) save_mem("norcpu-1-before.bin") mem_sz = len(mem) if len(mem) >= 0x10000: print "Too much code (%08X, %04X)" % (len(mem), len(mem) - 0x10000) sys.exit() # Inject plain password in the last moment (for testing). put_string(guess, "password") save_mem("norcpu-2-before-with-password.bin") if verbose: print "Original memory:" print dump(mem) start_time = time.time() norcpu() end_time = time.time() save_mem("norcpu-3-after.bin", mem_sz) if verbose: print "Memory after:" dump(mem) print print "Size: %X" % len(mem) print "Time: %d" % (end_time - start_time) print "Exit: %04X" % mem[exit_reg] print("CRC : %04X (%04X)" % (crc16, correct_crc)) result = names["result"] result_value = "" for i in range(0, mem[names["result_sz"]]): result_value = result_value + chr(mem[result + i] & 0xff) if result_value != secret_code: print "ERROR: [%s] != [%s]" % (secret_code, result_value) js = string.Template(open('template.html', 'r').read()) js = js.substitute( \ ip = names["ip"], exit_reg = names["exit_reg"], shift_reg = names["shift_reg"], password = names["password"], password_sz = names["password_sz"], result = names["result"], result_sz = names["result_sz"], mem_js = mem_js ) f = open("norcpu.html", "w") f.write(js) f.close()
Файл norcpu.py (template.html).
import sys, re, time, string, binascii verbose = False verbose_cpu = False scramble = True secret_password = "h1cKmE1fUsAn" secret_password_xor_mask = 0x3401 secret_password_add = 29 secret_code = "R0und2 D0ne!" secret_code_xor_mask = 0x730A secret_code_add = 37 guess = "123456789012" guess = secret_password code_segment = [] data_segment = [] label_count = 0 def dump(data, length = 8): result = [] for i in xrange(0, len(data), length): line = data[i:i + length] hex_line = ' '.join(["%04X" % x for x in line]) result.append("%04X: %-*s\n" % (i, length*5, hex_line)) return ''.join(result) def dump_js(data, length = 8): result = [] for i in xrange(0, len(data), length): line = data[i:i + length] hex_line = ' '.join(["0x%04X," % x for x in line]) result.append("%-*s\n" % (length*5, hex_line)) return ''.join(result) def encode_string(data, name, mask, coef_add): global mem, names offset = names[name] offset_sz = names[name + "_sz"] for i in range(0, len(data)): mem[offset + i] = ord(data[i]) ^ mask mask = (mask * 3 + coef_add) & 0xffff mem[offset_sz] = len(data) def put_string(data, name): global mem, names offset = names[name] offset_sz = names[name + "_sz"] for i in range(0, len(data)): mem[offset + i] = ord(data[i]) mem[offset_sz] = len(data) def save_mem(name, size = -1): f = open(name, "w") if size == -1: size = len(mem) for i in (mem[0:size]): hex = "%04X" % i bin = binascii.a2b_hex(hex) f.write(bin) f.close() def next_label(): global label_count label_count = label_count + 1 return "label_%04d" % label_count def code_rem(comment): code_segment.append('; ' + comment) def data_rem(comment): data_segment.append('; ' + comment) def data_label(name): data_segment.append(name + ":") def code_label(name): code_segment.append(name + ":") def code(value): printed = value if type(value).__name__ == 'int': printed = "%d" % value code_segment.append(" dw %s" % printed) scramble_counter = 0x2743 def next_scramble_counter(): global scramble_counter scramble_counter = scramble_counter * 3 + 7 return scramble_counter & 0xffff def word(value): if value == -1: if scramble: value = next_scramble_counter() else: value = 0 printed = value if type(value).__name__ == 'int': printed = "%d" % value data_segment.append(" dw %s" % printed) def buffer(length, value = -1): for i in range(0, length): word(value) def var(name, value = -1): data_label(name); word(value); # Macros def NOR(a, b, r): code_rem('NOR ' + str(a) + ' ' + str(b) + ' ' + str(r)) code(a) code(b) code(r) def NOT(a, r): NOR(a, a, r); def OR(a, b, r): NOR(a, b, "or_reg") NOT("or_reg", r) var("or_reg") def AND(a, b, r): NOT(a, "and_reg_a") NOT(b, "and_reg_b") OR("and_reg_a", "and_reg_b", "and_reg_a") NOT("and_reg_a", r) var("and_reg_a") var("and_reg_b") def ANDi(a, imm, r): MOVi(imm, "and_i_reg") AND(a, "and_i_reg", r) var("and_i_reg") def XOR(a, b, r): NOT(a, "xor_reg_a") NOT(b, "xor_reg_b") AND(a, "xor_reg_b", "xor_reg_b") AND(b, "xor_reg_a", "xor_reg_a") OR("xor_reg_a", "xor_reg_b", r) var("xor_reg_a") var("xor_reg_b") def XORi(a, imm, r): MOVi(imm, "xor_i_reg") XOR(a, "xor_i_reg", r) var("xor_i_reg") def MOV(a, b): code_rem('MOV ' + str(a) + ' ' + str(b)) NOT(a, "move_reg") NOT("move_reg", b) code_rem('MOV END') var("move_reg") def JMP(a): code_rem('JMP ' + str(a)) MOV(a, "ip") def JMPi(a): code_rem('JMPi ' + str(a)) label = next_label() JMP(label) code_label(label) code(a) def MOVi(imm, a): code_rem('MOVi #' + str(imm) + ' ' + str(a)) label_data = next_label() label_jump = next_label() MOV(label_data, a) JMPi(label_jump) code_label(label_data) code(imm) code_label(label_jump) # [a] -> b def PEEK(a, b): label1 = next_label() label2 = next_label() MOV(a, label1) MOV(a, label2) code_label(label1) # NOT(0, 0, move_reg) code(0) # <- a code_label(label2) # code(0) # <- a code("move_reg") # NOT("move_reg", b) # a -> [b] def POKE(a, b): code_rem('POKE ' + str(a) + ' [' + str(b) + ']') label = next_label() MOV(b, label) NOT(a, "move_reg") # +3 (three operations) code("move_reg") # +4 code("move_reg") # +5 code_label(label) code(0) # <- b # imm -> [a] def POKEi(imm, a): MOVi(imm, "poke_i_reg") POKE("poke_i_reg", a) var("poke_i_reg") def EXIT(a): MOV(a, "exit_reg") def EXITi(a): MOVi(a, "exit_reg") def FADD(mask, carry, a, b, r): AND(a, mask, "fadd_reg_a") # zero bits in 'a' except mask'ed AND(b, mask, "fadd_reg_b") # zero bits in 'b' except mask'ed AND(carry, mask, carry) # zero bits in 'carry' except mask'ed # SUM = (a ^ b) ^ carry XOR(a, b, "fadd_reg_t1") XOR("fadd_reg_t1", carry, "fadd_reg_bit_r") # Leave only 'mask'ed bit in bit_r. AND("fadd_reg_bit_r", mask, "fadd_reg_bit_r") # Add current added bit to the result. OR("fadd_reg_bit_r", r, r) # CARRY = (a & b) | (carry & (a ^ b)) AND(a, b, "fadd_reg_t2") AND(carry, "fadd_reg_t1", "fadd_reg_t1") # CARRY is calculated, and 'shift_reg' contains the same value # but shifted the left by 1 bit. OR("fadd_reg_t2", "fadd_reg_t1", carry) # CARRY is shifted the left by 1 bit to be used on the next round. MOV("shift_reg", carry) # shift_reg = mask << 1 MOV(mask, mask) # mask = shift (effectively "mask = mask << 1") MOV("shift_reg", mask) AND(carry, mask, carry) var("fadd_reg_a") var("fadd_reg_b") var("fadd_reg_bit_r") var("fadd_reg_t1") var("fadd_reg_t2") def ZERO(a): XOR(a, a, a) def FADC(a, b, r): ZERO("fadc_reg_t") MOV("const_1", "fadc_reg_mask") for i in range(0, 16): FADD("fadc_reg_mask", "carry_reg", a, b, "fadc_reg_t") MOV("fadc_reg_t", r) ZERO("fadc_reg_t") for i in range(0, 16): OR("fadc_reg_t", "carry_reg", "fadc_reg_t") MOV("carry_reg", "carry_reg") MOV("shift_reg", "carry_reg") MOV("fadc_reg_t", "carry_reg") var("fadc_reg_mask") var("fadc_reg_t") def ADD(a, b, r): ZERO("carry_reg") FADC(a, b, r) def ADDi(a, imm, r): MOVi(imm, "add_i_reg") ADD(a, "add_i_reg", r) var("add_i_reg") def PUSH(a): ADD("stack_reg", "const_minus_1", "stack_reg") POKE(a, "stack_reg") def PUSHi(imm): MOVi(imm, "push_i_reg") PUSH("push_i_reg") var("push_i_reg") def POP(a): PEEK("stack_reg", a) ADD("stack_reg", "const_1", "stack_reg") def CALL(a): label = next_label() PUSHi(label) JMP(a) code_label(label) def CALLi(a): label = next_label() PUSHi(label) JMPi(a) code_label(label) def RET(): POP("ip") # Jump 'a', if cond = FFFF, and 'b' if conf = 0000 def BRANCH(a, b, cond): AND(a, cond, "branch_reg_a") # reg_a = a & cond NOT(cond, "branch_reg_b") # reg_b = !cond AND(b, "branch_reg_b", "branch_reg_b") # reg_b = b & reg_b = b & !cond OR("branch_reg_a", "branch_reg_b", "ip") # ip = (a & cond) | (b & !cond) var("branch_reg_a") var("branch_reg_b") # Jump 'a', if cond = FFFF, and 'b' if conf = 0000 def BRANCHi(a, b, cond): MOVi(a, "branch_i_reg_a") MOVi(b, "branch_i_reg_b") BRANCH("branch_i_reg_a", "branch_i_reg_b", cond) var("branch_i_reg_a") var("branch_i_reg_b") # if a != 0 -> carry = FFFF else carry = 0000 def IS_0(a): ZERO("carry_reg") FADC(a, "const_minus_1", "is_0_reg") NOT("carry_reg", "zero_reg") var("is_0_reg") var("zero_reg") # ip = (zero_reg == FFFF ? a : ip) def JZi(a): label = next_label() BRANCHi(a, label, "zero_reg") code_label(label) # ip = (zero_reg == FFFF ? a : ip) def JNZi(a): label = next_label() BRANCHi(label, a, "zero_reg") code_label(label) def ROL(a, b): MOV(a, a) # shift_reg = a << 1 MOV("shift_reg", b) def ROR(a, b): MOV(a, "ror_reg") for i in range(0, 15): ROL("ror_reg", "ror_reg") MOV("ror_reg", b) var("ror_reg") def SHL(a, b): ROL(a, b) ANDi(b, 0x0001, b) def SHR(a, b): ROR(a, b) ANDi(b, 0x7FFF, b) def MUL3(a, b): ADD(a, a, "mul3_reg") # mul3_reg = a + a ADD("mul3_reg", a, b) # b = mul3_reg + a var("mul3_reg") # NORCPU code var("ip", "start") var("shift_reg") var("carry_reg") var("const_1", 1) var("const_minus_1", 0xFFFF) var("exit_reg") var("stack_reg", "stack") code_label("start") var("ch") var("t") var("xor_mask") var("cmp_flag") var("ptr") var("ptr2") var("i") MOVi("exchange", "ptr") MOVi("secret_password", "ptr2") MOVi(secret_password_xor_mask, "xor_mask") MOVi(0, "cmp_flag") MOVi(len(secret_password), "i") cmp_loop = next_label() code_label(cmp_loop) # cmp_loop: PEEK("ptr", "ch") # ch = *ptr XOR("ch", "xor_mask", "ch") # ch ^= xor_mask PEEK("ptr2", "t") # t = *ptr2 XOR("ch", "t", "ch") # ch = ch ^ t OR("cmp_flag", "ch", "cmp_flag") # cmp_flag |= ch ADD("ptr", "const_1", "ptr") # ptr += 1 ADD("ptr2", "const_1", "ptr2") # ptr2 += 1 MUL3("xor_mask", "xor_mask") # xor_mask *= 3 ADDi("xor_mask", secret_password_add, "xor_mask") # xor_mask += add_const ADD("i", "const_minus_1", "i") # i -= 1 IS_0("i") JNZi(cmp_loop) MOVi(0, "exchange_sz") ok_label = next_label() IS_0("cmp_flag") JZi(ok_label) exit_label = next_label() JMPi(exit_label) code_label(ok_label) MOVi("secret_code", "ptr") MOV("secret_code_sz", "i") MOVi(secret_code_xor_mask, "xor_mask") MOVi("exchange", "ptr2") MOV("i", "exchange_sz") loop = next_label() code_label(loop) # loop: PEEK("ptr", "ch") # ch = *ptr XOR("ch", "xor_mask", "ch") # ch ^= xor_mask POKE("ch", "ptr2") # *ptr2 = ch MUL3("xor_mask", "xor_mask") # xor_mask *= 3 ADDi("xor_mask", secret_code_add, "xor_mask") # xor_mask += add_const ADD("ptr", "const_1", "ptr") # ptr += 1 ADD("ptr2", "const_1", "ptr2") # ptr2 += 1 ADD("i", "const_minus_1", "i") # i = i - 1 IS_0("i") JNZi(loop) code_label(exit_label) # exit_label: EXITi(0x00) buffer(8) data_label("stack") var("secret_code_sz", len(secret_code)) data_label("secret_code") buffer(len(secret_code)) var("secret_password_sz") data_label("secret_password") buffer(16) var("exchange_sz", 0) data_label("exchange") buffer(32) # Compiler text = code_segment text.extend(data_segment) if verbose: print "\n".join(text) # Phase 1. Calculate names. addr = 0 names = {} for line in text: if line[0] == ';': continue if line[0] != ' ': name = line.partition(':')[0] names[name] = addr else: addr = addr + 1 if verbose: print names raw_text = "\n".join(text) # Resolve names. for name in names: if verbose: print name, names[name], type(names[name]) name_re = re.compile(r'dw ' + name + '$', re.M) value = "%d" % names[name] raw_text = name_re.sub('dw ' + value, raw_text) text = raw_text.split("\n") if verbose: print "\n".join(text) # Phase 2. Compilation. addr = 0 comment = "" mem = [] for line in text: if line[0] == ';' or line[0] != ' ': comment = comment + line + ' ' else: value = int(line.strip().partition(" ")[2]) if verbose: print "%04X: %04X ; %s" % (addr, value, comment) mem.append(value) addr = addr + 1 comment = "" # Interpretation ip = names["ip"] exit_reg = names["exit_reg"] shift_reg = names["shift_reg"] carry_reg = names["carry_reg"] def nor(a, b): r = a | b r = r ^ 0xFFFF return r & 0xFFFF def norcpu(): while 1: i = mem[ip]; a = mem[i + 0] b = mem[i + 1] r = mem[i + 2] mem[ip] = i + 3 f = nor(mem[a], mem[b]) mem[r] = f mem[shift_reg] = ((f >> 15) & 1) | ((f & 0x7FFF) << 1) if verbose_cpu: print "%04X: %04X [%04X] %04X [%04X] -> %04X [%04X]" % \ (i, a, mem[a], b, mem[b], r, mem[r]) if r == exit_reg: break print "Starting from [%04X]" % mem[ip] encode_string(secret_code, "secret_code", secret_code_xor_mask, secret_code_add); encode_string(secret_password, "secret_password", secret_password_xor_mask, secret_password_add); mem_js = dump_js(mem) save_mem("norcpu-1-before.bin") mem_sz = len(mem) if len(mem) >= 0x10000: print "Too much code (%08X, %04X)" % (len(mem), len(mem) - 0x10000) sys.exit() # Inject plain password in the last moment (for testing). put_string(guess, "exchange") save_mem("norcpu-2-before-with-password.bin") if verbose: print "Original memory:" print dump(mem) start_time = time.time() norcpu() end_time = time.time() save_mem("norcpu-3-after.bin", mem_sz) if verbose: print "Memory after:" dump(mem) print print "Size: %X" % len(mem) print "Time: %d" % (end_time - start_time) print "Exit: %04X" % mem[exit_reg] exchange = names["exchange"] result_value = "" for i in range(0, mem[names["exchange_sz"]]): result_value = result_value + chr(mem[exchange + i] & 0xff) print "Result: [%s]" % result_value if len(result_value) == 0: print "ERROR: Wrong password" js = string.Template(open('template.html', 'r').read()) js = js.substitute( \ ip = names["ip"], exit_reg = names["exit_reg"], shift_reg = names["shift_reg"], exchange = names["exchange"], exchange_sz = names["exchange_sz"], mem_js = mem_js ) f = open("norcpu2.html", "w") f.write(js) f.close()]]>
Мне нравится этот контест тем, что он не заточен исключительно на алгоритмические задачи. Например, в Q надо было сделать хорошую эвристику, в R - типа взломать код, ну а S - это алгоритм.
Я по-пенсионерски попросился решать R. В итоге написал какой-то невообразимый велосипед на С++, которым таки получилось сделать играемый wav, прослушать сообщения и сдать задачу, и только потом, немного успокоившись, на питоне получилась короткая программа, делающая чистые 16-битные wav’ы.
import wave, struct def make_soundfile(sample, freq, fname): frate = 7000 data_size = len(sample) sine_list = [] for x in sample: sine_list.append(float(x) / 2 * 32768) wav_file = wave.open(fname + ".wav", "w") nchannels = 1 sampwidth = 2 framerate = int(frate) nframes = data_size comptype = "NONE" compname = "not compressed" wav_file.setparams((nchannels, sampwidth, framerate, nframes, comptype, compname)) for s in sine_list: wav_file.writeframes(struct.pack('h', int(s))) wav_file.close() for n in range(1, 11): fname = "test-%02d.tst" % n print fname sample = open(fname).readlines() sample = sample[1:] freq = len(sample) make_soundfile(sample, freq, fname)
Конечно, авторское решение было еще проще.
awk 'NR > 1 {printf "%c",int(128*($1+1))}' *.in >/dev/dsp
Погдядим, что будет в реальном раунде в эту субботу.
]]>Но когда я узнал причину - вот это действительно вызвало у меня минуту молчания.
Причина - это антивирус, а точнее его “активная защита”, которая проверяла каждый божий файл (а их там тысячи), генерируемый при сборке. Просто забыли ее выключить при установке системы.
Получается, в принципе, пишу я вирус, компилирую, а тут бабах - и при сборке мне говорят, что результат вашей компиляции был убит, убит еще до того, как успел в первый раз родиться, так как бинарь содержит код, похожий на вирус (я же вирус пишу!).
Ведь антивирус не знает, является ли этот новый файл просто результатом компиляции из исходника, или файл скачивается из интернета.
В общем, отключили антивирус – сборка ускорилась в разы.
]]>Вот мой список:
Как я понял, подобные проекты должны быть большими по размеру, ибо только на огромном количестве исходников проявляется умение постоянно кодировать красиво и единообразно.
Увы, такие примеры попадаются нечастно.
Есть бесчисленное множество почти хороших исходников, но в этот список я поместил свои случаи без “почти”.
У вас есть подобные примеры?
Будем добавлять в список.
]]>В задаче, что предлагаю я, программы все еще можно писать вручную на некотором высокоуровневом макроассемблере.
Итак, имеется модель некоторого виртуального процессора, выполняющего только одну логическую операцию - Стрелку Пирса.
На этом процессоре написана программа, на вход которой подается некоторый пароль. Если пароль неверный, то в ответ выдается строка “Wrong password!”. Если верный, то выдается определенное волшебное сообщение.
Задача: любым образом выяснить это волшебное сообщение. Как вариант, можно, например, угадать пароль, и программа сама выдаст секрет.
Логика написана таким образом, что разобравшись в алгоритме, можно без труда расшифровать волшебное сообщение.
В прошлом году я описал использованный подход во всех деталях.
Оригинальный подход, на котором основан мой эксперимент, был не совсем “чистым”, так как команда сложения был вынесена за логику процессора. В моей версии все до единой команды реализованы на самом процессоре. Для этого потребовалось немного изменить интерпретатор, добавив в него сдвиговый регистр.
Для желающих попробовать взломать мой эксперимент, я сделал страничку, на которой на JavaScript’e реализован выше описанный виртуальный процессор с одной командой и программа для него, проверяющая пароль.
Итак, прошу на взлом!
Удачи.
P.S. Для первого взломавшего - небольшой приз! Информация по ссылке.
]]>Это здоровая реакция нормального разработчика, бесконечно стремящегося к совершенству кода. Но, увы, в больших системах, которые разрабатывались годами или даже десятилетиями нельзя просто так взять и все переписать. Бизнес не даст этого сделать.
Вот и возникает волшебное слово «legacy» код.
Лично на своем опыте я убедился, что работы со «старым» - это умение, и умение очень полезное. Парадокс, но порой выгоднее взять старый код, работающий годами, и просто обернуть его красивый интерфейс текущего «правильного» языка, чем тратить титанические усилия на полностью новую разработку.
Ну и что, что Фортран с common-переменными повсюду или С, ну что, что там код 80-х годов с функциями на десятки экранов без каких-либо зачатков unit-тестирования, и потрогать этот код просто страшно. Но трогать его не надо, а надо понять интерфейс и изолировать его.
Лично у меня подобное «прозрение» случилось только недавно, примерно аналогично как с unit-тестированием. Слово «legacy» код не должно синонимом «стереть и переписать все нафиг!», а должно быть знаком того, что надо сделать взвешенную оценку поддержки старого «тлеющего» кода против новой разработки. Любой код начинает тлеть сразу поле релиза, но когда он вам уже достался тлеющим – это другая история.
А корень парадокса в том, что пользователи не хотят вашу новую версию, переписанную с нуля на модном языке Х, они хотят проверенную версию, что далеко не всегда означает одно и то же.
Как пример, банки – это одни из консервативнейших потребителей программных продуктов. И их можно понять – фаза тестирования перед внедрением на боевую обычно длится месяцами и стоит приличных денег, а уж после того, как системы запущена, никто не будет ничего менять без веской причины. А слова «мы тут все переписали заново, пожалуйста, поставьте…» обычно игнорируются на слове «все».
В общем, если хотите рассматривать создание программных продуктов не просто как написание «идеального кода», а как бизнес, работа со старым кодом – это один из навыков, который придется освоить.
]]>Лично я люблю git для UNIX и mercurial для Windows. Каждая система имеет пачку удобных хостингов, как говорится, выбирай на вкус.
И вот у меня образовался закрытый проект, который мало того, что активно развивается, так еще и накапливает баг-репорты, приправленные файлами отчетов и картинками, и требует ведения документации.
Начал я его вести в mercurial, но когда начал утопать в письмах и документации, то осознал необходимость баг-трекера и wiki. Настраивать все это локально (на публичные готовые хостинги выложить не могу) как-то лень. И тут я вспомнил по fossil.
Fossil - это распределенный контроль версий, баг-трекер и wiki в одном флаконе. Более того, его автор – ни кто иной, как автор SQLite, борец за минимализм, простоту и надежность. Как и в случае с небезызвестной базой данных, которую кто уже только не использует, все, что вам нужно - это один единственный файл – fossil[.exe]
.
Для командной строки - это просто SCM, а будучи запущенной с параметром “ui”, превращается в локальный веб-сервер, в котором есть “морда” для просмотра репозитория, баг-трекера и wiki. Более того, все данные живут также в одном единственном файле-репозитории, который по сути является SQLite-базой. Для переноса его в другое место, другую операционную систему или резервного копирования, нужно просто скопировать один файл.
fossil умеет импортировать и экспортировать в git, поэтому я сначала перегнал существующий репозиторий из mercurial в git, а потом импортировал из git в fossil.
В целом, fossil хорош. Вылизанный и минималистичный. Говорят, что из-за использования SQLite в качестве хранилища, с одной стороны получаешь надежность и транзакционность любых изменений (понятно, что хоть остальные системы работают просто с файлами, у них с целостностью тоже все в порядке), но с другой стороны, по скорости может радикально проигрывать git или mercurial на больших проектах. Но для небольших “домашних”, но секретных проектов - сложно представить удобнее утилиты.
Даже в рамках компании, можно личный проект в два счета превратить в общий, просто запустив fossil в режиме сервера и дав коллегам его адрес. Домашняя страничка fossil по сути является сервером, работающим на fossil (там можно увидеть живой трекер и wiki). Не самое плохое доказательство уверенности автора fossil’а в своем детище.
Да, лицензия у fossil, конечно, BSD.
Итак, для дома для семьи – очень удобно.
]]>c:\lcc\bin\lcc -v
Logiciels/Informatique lcc-win32 version 3.8. Compilation date: Dec 4 2010 13:14:58
Файл: t.c
:
int main() { char* p; char* s[1] = { p }; }
c:\lcc\bin\lcc t.c
Error t.c 3 Compiler error (trap). Stopping compilation
Обычно ж как бывает, начинает проявляется “баг компилятора” – программа ведется себя странно, исключения почему-то не ловятся, наблюдаются неожиданные падения программы и т.д. В подавляющим случаев, увы, все кончается просто ошибками работы с памятью. Ничего сверхестественного.
Баги же типа этого, проявляющиеся на тривиальном примере - это всегда событие.
Из недавнего:
]]>Также на многократном личном опыте знаю, что когда тебе оказывают – это всегда обидно и досадно, какой бы причина там ни была. Но это случается почти со всеми.
К сожалению, отказывать приходится порой из-за радикально тривиальных вещей, незнание которых просто несовместимо с профессией.
У меня накопилось несколько вопросов, незнание ответа на которые является почти стопроцентной причиной, когда я в своем отчете писал отказ.
Интервьюирование было на позицию “Senior C/C++ developer”.
Ответы приведу тут же, так как они очевидны.
1. Сколько примерно будет 2^32? (обычно задается по телефону)
Ответ “Около четырех миллиардов” является исчерпывающим.
Я вообще и не могу понять, как человек, в названии профессии которого есть слово “программист” может этого не знать. Увы, это далеко не единичные случаи.
2. Как сравнить две переменные типа double или float на равенство? (обычно задается по телефону)
Ответ “Вычесть одно из другого и сравнить результат на больше/меньше с каким-то малым числом, например 10E-6” является исчерпывающим. Конечно, много зависит от используемой библиотеки работы с числами с плавающей точкой, но смысл, в целом, одинаков.
Увы, количество неотвечающих тоже весьма значительно.
3. (Хит!) Что распечатает данная программа? (не забываем, что собеседование на позицию разработчика на C/C++). В принципе, его тоже можно задать по телефону.
char* f() { char buf[100]; strcpy(buf, "TEST”); return buf; } int main() { char* s = f(); /* (1) */ printf("%s\n", s); }
Ответ: “Нельзя сказать с уверенностью, скорее всего мусор, но в целом это неопределенное поведение, так как локальный буфер формально прекращает существование после выхода из функции f()” является почти исчерпывающим.
Почему “почти”? Потому что обычно за ним дополнительный вопрос: “Что именно может с высокой вероятностью затирать заветное слово TEST и приводить к выводу мусора? Для конкретности: платформа x86, 32-bit, компилятор Visual Studio. Если остановить программу отладчиком в точке (1) и посмотреть, на что указывает указатель “s”, то очень высока вероятность, что там будет таки “TEST”, а вот printf() таки с высокой вероятностью распечатает мусор. Почему?“.
Более половины собеседований, в которых я участвовал, заканчивались со знаком “минус”, так как человек даже не делал попытку сказать что-то типа “В данных условиях скорее всегда слово TEST будет перезатерто параметрами функции printf(), которые передаются через стек и ложатся на то место, где был раньше размещен буфер “buf”. Конечно, многое зависит от режимов оптимизации, так как аргументы могут быть переданы через регистры.”
Фактически, произнесенные слова “стек” и “параметры функции” являются достаточным ответом на вопрос.
Повторюсь, я лично считаю, что на собеседовании задача интервьюера не показать, на сколько он сам умен (читать задачи по бумажке может каждый), а попытаться разглядеть в собеседнике те качества, которые требуются для данной позиции. И просто отказывать человеку из-за того, что он не ответил, как тебе кажется, на один элементарный вопрос, также глупо, как и делать из этого вывод о прочих знаниях кандидата.
Но все же есть такая черта, ниже которой уже нельзя.
А у вас есть вопросы, “неответы” на которые вы лично можете считать поводом для практически однозначного отказа?
]]>Как-то яростно пытался доказать, что тип int является за 100% атомарным без каких-либо гвоздей на платформе x86. Не доказал (и вот почему).
Вопрос: «При множественном виртуальном наследовании в С++, кто и как должен заботиться о правильном однократном вызове конструктора базового класса (подразумевается, что у этого конструктора есть параметр)?». Я сказал, что это компилятор, и вдобавок еще не смог объяснить почему.
Ну и лидер нашего хит-парада. Вопрос: «В С++ таблица виртуальных функций принадлежит классу или экземпляру класса?» Барабанная дробь: я сказал, что каждый экземпляр одного класса имеет собственную копию таблицу. Почему я так сказал, я до сих пор не знаю.
Все это еще раз подтверждает факт того, что на нервах можно сморозить такое, что потом сам будешь думать о причинах сказанного.
]]>И я был приятно удивлен практически всему увиденному. Во всего шести мегабайтах дистрибутива вы получаете быстрый компилятор С99 с поддержкой современных процессоров, линковщик, ассемблер, компилятор ресурсов и внушительную библиотеку.
Про библиотеку хочу сказать отдельно. Помимо стандартного набора libc и Win32 API, там полно всего остального. Лично я был несказанно удивлен простой, и порой столь нужной функцией ping()
(и не надо больше вызывать ping.exe
в скрытом окне).
В общем, с помощью также идущих в комплекте регулярных выражений, я быстро подхачил putty как мне было нужно. Попутно проронил ностальгическую слезу от программирования оконного интерфейса на чистом Win32 API и ощутил некоторые приятности С99. Например, объявление переменных не в начале блока, а где удобно, и размер автоматических массивов задавать не статически, а из переменной. C99 однозначно стоит внимательного изучения.
Приведу небольшую выжимку из идущих в комплекте библиотек (кроме стандартных libc и Windows API, конечно). Думаю, названия говорят сами за себя.
gl.h OpenGL
sqlite.h
bignums.h Работа с числами произвольной точности
bitstring.h
bluetoothapis.h
d3d.h
d3dx.h
dynloader.h Работа с DLL’ками
gc.h Сборщик мусора (требует запуска, конечно)
getopt.h
icmpapi.h
int128.h
matrix.h Работа с векторами и матрицами
mq.h IBM MQ
msi.h
netmon.h
netsh.h
pcre.h Регулярные выражения в стиле Perl
ping.h PING!
ras.h
regexp.h Простой API для регулярных выражений (regcomp() и regexec())
snmp.h
sqlite3.h
str.h Работа со строками в стиле C99
tapi.h
Итак, если вам быстро нужен небольшой компилятор (дистрибутив всего шесть мегабайт), для написания программы на С99 под Windows (для графического интерфейса придется все делать на чистом Win32 API), имеющий в комплекте в дополнение к libc и Win32 API приличный набор разнообразных библиотек, то LCC – это очень сильный кандидат.
Кстати, отдельно можно скачать и 64-битную версию компилятора.
Единственное, чего я не пробовал – это линковать объектники LCC с другими компиляторами. Кто имеет опыт – поделитесь.
]]>http://e-maxx.ru/algo/ - алгоримты на С++ с минимумом теории http://sites.google.com/site/indy256/ - алгоритмы на Java и С++
Если у вас есть в заначке ссылка на ресурс подобного качества, поделитесь пожалуйста.
]]>Распространенный подход – это учет рабочего времени. Программистов обязывают отмечать, сколько времени было потрачено на выполняемые задачи. Более того, бывает, что эта суммарная цифра должна быть не меньше, заявленной в трудовом договоре. Подписался на 45 часов в неделю, изволь представить недельный отчет на эти часы как минимум.
Например, типичный отчет за день:
2 часа встреча по проекту Х
1 час недельная летучка всего отдела
4 часа работа над багфиксом #ААААА
1 час телефонное интервью
1 час работа над проектом Y
В целом, замечательное начинание с прекрасной целью. Множество полезной статистики можно вынести из такой отчетности: багфиксы какого клиента занимают сколько времени, каково соотношение между багфиксами и разработкой, какой проект явно требует больше усилий, чем планировалось и т.д.
Но есть тут одно “но”. Лично на моем опыте могу сказать, что у программиста ничего кроме нервотрепки обязанность делать такие отчеты не вызывает. Обычно это кончается решением задачи поиска дырки, куда бы приткнуть лишнее время, так как на проект биллить неохота из-за уже зашкаливающего коэффициента «Забиллено/Запланировано».
Но такая отчетность нужна в том или ином виде, и этой работой должен заниматься не программист, а его менеджер, и не формально, пытаясь понять, выполняются ли контрактные 45 часов, а неформально, не привязывая итоговые цифры по проекту/отделу конкретно к программисту.
Вы спросите, как тогда оценивать эффективность самого программиста? Уж точно не по часовой разблюдовке его дней, а по комплексному показателю: качество выполненных проектов, отзывы клиентов или партнеров, отзывы коллег и т.д.
Качество формально насаженного обязательства отмечать рабочее время обычно оставляет желать лучшего. Только если сам программист понимает и видит, как его отчеты помогают компании правильно планировать, может представлять действительно полезные отчеты, я не формалистику «чтобы ко мне не приставали».
А у вас в компании есть “биллинг” времени?
]]>Буду признателем за информацию на подобные ресурсы. Обновления буду добавлять в пост.
]]>Чтобы найти драгоценное зерно в такой куче, нужна правильная утилита для просмотра, а главное, поиска (типа «дай-ка я гляну, как народ эту функцию вызывает?» или «как там правильно создать экземпляр этого класса?» и т.д.).
Большинство систем контроля версий имеют веб-интерфейс для подобных целей. Также есть независимые системы, и одна из них называется Opengrok.
Это система с открытым кодом под лицензией CDDL. Может индексировать репозитории почти всех основных систем контроля версий, а для некоторых понимает и историю файлов. Множество критериев поиска. Крайне полезно, что можно одновременно подключать для индексирования несколько репозиториев, причем от разных VCS. Естественно, при просмотре исходник представляется гипертекстовым документом, через который можно двигаться дальше.
Кстати, можно вживую пощупать Opengrok на исходниках OpenSolaris’а.
В общем, лично у меня крайне положительный опыт работы с этой системой на весьма значительной по размеру, разнообразию языков и подключенных одновременно VCS кодовой базе. Всячески рекомендую.
P.S. Я как-то в целом давно не писал про всякие организационные примочки, облегчающие работу программиста.
Поэтому, небольшой списочек из старенького, но все еще актуального:
]]>int
(да и любого типа, равного по длине шине процессора, например, указателя или float
для x86).
Ясно, что в теории, нельзя полагаться на факт такой атомарности. Но давайте конкретизируем: платформа x86, и переменная объявлена как volatile int a
. Тут я не играюсь с reinterpret_cast
’ом и приведением указателей, то есть можно гарантировать, что компилятор обеспечит правильное выравнивание, соответствующее шине процессора и памяти, тем самым гарантируя, что доступ к этой ячейке произойдет за один такт.
Есть ли хоть какой-то шанс с ненулевой вероятностью, что какое-то вычисление (команда процессора) по отношению к а
может быть тут неатомарна? Может ли так быть, что операция a++
или a += arbitrary_stuff
и т.д. выполниться не целиком?
Так как переменная volatile
, значит любые оптимизации будут компилятором запрещены, и не выйдет так, что вместо полноценной 32-х битной команды (обнуления, инкремента, умножения и т.д) компилятор использует, например, каскад двух 8-ми битных команд для операции, которую можно сделать одной 32-х битной.
Ведь где бы значение переменной а
не обрабатывалось (в регистре, в кэше, в памяти), везде это будет та или иная одиночная команда процесса, которая, очевидно, атомарна.
Ясно, что правилом хорошего тона считается не полагаться на атомарность int
’а. Но современная архитектура процессоров (микроконтроллеры пока не берем) практически гарантирует эту атомарность, разве нет?
Что это такое? Это весьма продвинутый движок комментирования (с шахматами и гимназистками) со множеством современных примочек на замену стандартному: сортировка при просмотре, кнопки Не/Нравится, ответ на конкретный комментарий. Ну и разные административные/модераторские возможности.
В целом, ничего особенно, но по сравнению со стандартным движком Блогспота - это небо и земля.
Установка, удивительно, простая. Вообще ничего делать не надо - просто указать имя блога на Blogger’е и разрешить доступ на модификацию шаблона. Далее все происходит автоматически. Вроде даже существующие комментарии проимпортировались.
В общем, посмотрим. Будут глюки - пишите.
UPDATE: Оказывается, тут можно редактировать собственные комментарии. Ура!
]]>Обычно рекрутеры объясняют причину этого вопроса так, мол, это требование клиента, так как клиент хочет понимать, какой потенциальный скачок зарплаты ты себе хочешь. И часто, когда я пытался сопротивляться ответу на этот вопрос, агенты говорили, что тогда они не смогут дальше продвигать мое резюме клиенту. Часто люди на это ведутся, как велся и я.
Сейчас, имея приличный опыт общения с различными рекрутерами, я могу сказать только одно: никогда не надо сообщать рекрутерам своей текущей зарплаты. Достаточно вежливо сказать, что это, раз, личная информация, и, два, она не имеет никакого отношения к вопросу потенциального устройства на новую работу.
Почему компании хотят знать вашу текущую зарплату? Все просто. После того, как успешно прошел все собеседования, и принято решения тебя брать, в работу включаются внутренние рекрутеры компании, цель которых нанять человека за возможно меньшие деньги. И если вдруг выходит, например, что ты пытаешься договориться на сумму, в два раза превышающую, что было раньше, это может быть еще одним доводом при обсуждении зарплаты, чтобы ее сбить (типа «где вы видели повышения зарплаты сразу в два раза?»). Действительно, для рынка труда в Англии это действительно маловероятное повышение, но, опять-таки, к переговорам это не имеет никого отношения.
Далее, тема того, что если не сообщать рекрутинговому агенту своей зарплаты, то он типа не сможет подать твое резюме в ту или иную компанию. Это самый натуральный блеф. У меня лично было несколько случаев, когда два разных агента предлагали позицию в одной и той же компании, обоим мной было отказано в вопросе о зарплате, и один сказал, что дальше работать со мной не будет (и был послан), а второй все прекрасно сделал и без знания моей текущей зарплаты.
Ну уж а если так выходит, кто представители компании вам прямо говорят, что если вы не скажете нам свою текущую зарплату, мы отклоним ваше резюме – это верный знак, что в такую компанию не стоит идти работать (на моем опыте пока такого не было).
И еще парадокс в том, что подавляющем большинстве компаний зарплата является крайне секретной информацией и всегда обсуждается сугубо персонально. Поэтому сообщать ее каким-то рекрутерам было бы крайне странно.
Есть небольшое замечание, что конкретно в Англии, в процессе оформления документов при поступлении на работу (контракт, всякие кодексы, соглашения о неразглашении и т.д.) ты должен предоставить формальные налоговые выписки о твоих доходах с предыдущего места работы (по ним можно понять зарплату), чтобы бухгалтерия правильно рассчитала тебе налоги, но все это происходит уже после, когда контракт уже согласован и подписан, и уже формально взят на работу.
Вот.
Кстати, может кто-нибудь поделиться опытом устройства на работу через кадровые агентства в России и других республиках и государствах нашего региона?
]]>Для начала – это вызов. Задача, которую надо решить (ведь помните, что мы должны быть разносторонними, а не только на С++ программировать). Это как участвовать в ТопКодере. У меня это всегда легкое (а порой и не очень легкое) волнение и ощущение бабочек внизу живота. Предлагаемая задача не решается в лоб академическими знаниями и всегда требует, чтобы ты себя проявил.
Затем идет чисто практическая сторона – опыт, который никаким другим способом не получить. Даже если тебя все устраивает на текущей работе или собственном бизнесе, всякое может случиться, и иметь опыт устройства на работу (как бы формально и прагматично это не звучало) иметь стоит. К тому же усилий для его получения надо не так и много. Чтобы успешно пройти интервью – надо уметь это делать. Хоть многие компании и заявляют, что они целенаправленно набирают специалистов, а не специалистов по устройству на работу – это блеф. Люди всегда оценивают людей. И порой надо догадаться, что за критерии отбора скрыты за вопросами и задачами, предлагаемыми тебе на интервью.
Например, многие думают, что все интервьюеры – это супер/мега/экстра спецы, которые видят тебя насквозь. И причина этому – просто что «он» задает тебе вопросы, а не ты ему. Хотя в большинстве случаев, особенно в больших компаниях, интервьюируют обычные разработчики (а некоторые и без особого желания), и если догадаешься, что ему надо – успех гарантирован.
Лично я понял, увы, не сразу, насколько важно быть не только технически подкованным, но и собрать максимально информации и компании, а по возможности и о предстоящем интервьюере. Благо сейчас у всех есть блоги, ЖЖ, Фейсбук, ЛинкедИн, МайСпейс и т.д.
А любой, даже самый позорный провал – это крайне полезная пища для самоанализа и понимания путей развития. Лично я всегда тщательно анализирую все интервью, разбираю задачи, на которых облажался, и много раз это возвращалось – люди ленивы, и не все интервьюеры утруждаются придумыванием задач, так что шанс получить однажды нерешенную задачу в другом месте очень даже велик.
Ну и под занавес – наблюдение процесса интервью со стороны кандидата позволяет лучше интервьюировать самому. Быстро начинаешь понимать цену, смысл и назначение тех или иных вопросов. Мне это очень помогает при интервьюировании в Блумберге.
Ладно, это лирика.
Хочу поделиться интересным примером.
Интервьюировался я недавно в одной небольшой трейдинговой фирме на позицию обычного разработчика на С++. Сначала было очень короткое пятнадцатиминутное телефонное интервью, которое я легко прошел (но увы, я не вынес из него всей полезной информации – см. ниже). Стандартный набор: С++, multithreading и пару вопросов про сложность. Под занавес товарищ ненавязчиво спросил, какими скриптовыми и функциональными языками я в принципе владею и интересуюсь.
Затем, они мне прислали домашнее задание.
Суть задачи: есть шахматная доска MxN и набор фигур: короли, ферзи, слоны, кони и ладьи (каждой фигуры есть некоторое количество). Пешек нет, и цвет фигур не имеет значения. Надо сгенерировать все возможные расстановки данных фигур на поле. В расстановке должны участвовать в точности все данные фигуры.
Было дано несколько простых тестовых данных. Для задачи большой размерности (поле 7 на 7, 2 короля, 2 ферзя, 2 слона и одна ладья) надо только вывести количество возможных позиций.
И далее было самое главное ограничение: можно писать задачу на любом языке, кроме C, C++, C#, Java, Python, PHP, Pascal.
Срок – неделя.
Я сначала почесал репу (вроде все выглядит как перебор с возвратами, и надо только позаботиться об исключении повторяющихся конфигураций), написал все сначала, конечно, на С++, отладил алгоритм. И стал думать, на чем мне сдавать задачу. Решил на Go. Написал, проверил. Результаты совпадают.
Отправил. Приходит ответ, что все неплохо, но типа мы ищем человека с более оригинальным подходом, чем использование С-подобного языка Go, когда мы явно не рекомендовали использовать императивные языки, тем более из семейства С, и ожидали реализацию на каком-то скриптовом или функциональном языке. Увы, отказ.
Ну, в общем, суть понятна. Задача была не в задаче, а в «показать себя разносторонним программистом», не который если что, так расчехляет С++.
Я поблагодарил их за время, а себя записал еще одно поражение из-за недостаточного анализа требований, пусть и весьма расплывчатых.
Вот.
P.S. Для желающих – исходники моей оригинальной программы на С++. Там же есть версия на Go, но плюсовый вариант содержит самый быстрый алгоритм, который я сумел придумать.
У меня больше нет идей, чтобы еще можно ускорить.
Желающие могут попробовать. Было бы очень интересно время работы теста #3.
Тесты (можно и добавлять свои) и проверяющая система уже встроены прямо в исходник. Его можно скомпилировать:
cl /O2 /EHsc problem.cpp
У меня печатается:
Case #0 OK (line 235)
Case #1 OK (line 248)
Case #2 OK (line 271)
Case #3 OK (line 318) time 1.495s
Для своей версии вам надо переписать функцию solve()
. Присваивая переменной problem_filter
значения, отличные от -1
, можно запускать не все тесты, а по одному.
Вообще, я спользую эту мини проверяющую систему для олимпиадных задач. Предложения приветствуются.
]]>А как насчет вопроса «а на каком языке вы наиболее продуктивны»?
Например, спортивное программирование, где нужно как можно быстрее выдать работающий код, нагруженный часто сложными алгоритмами.
Кстати, есть распространенное заблуждение, что спортивное программирование – это обычно куча корявого и запутанного, хоть и работающего кода, который совершенно непригоден для «обычного» программирования. Это не так, и зависит от конкретного человека. Можно, например, посмотреть код двух ТопКодеровских лидеров – ACRush и Petr. Код второго очень чистый, начиная от нормального форматирования и заканчивая общей понятной структурой решения. Код же первого же – обычно по внешнему виду оставляет желать лучшего (хотя это нисколько не умаляет его бесспорного лидерства), и читать его гораздо сложнее.
Так вот, я задал себе вопрос – а на каком языке я бы мог участвовать в контестах, где идет учет времени, и нет возможности копаться в документации по языку или библиотеке? И честно говоря, С++ находится далеко впереди Java и C# (Си не берем из-за бедности стандартной библиотеки). Мой уровень «погружения» в С++ (моменты языка, бесчисленные шаблоны уже виденного кода, знание библиотеки и т.д.) не сравним с той же Java.
А сколько языков у вас «живут в пальцах»?
]]>Проблема: использовать ли не константные ссылки в аргументах функций?
Мой подход: нет.
Почему? Причина тут только одна: использование неконстантной ссылки для аргумента функции скрывает в вызывающем коде факт возможной модификации объекта, передаваемого в качестве параметра.
Лично нарвался недавно на собственную мину (конечно, упрощенный вариант):
T a = 10; ... f(a); ... assert(a == 10); // BOOM!!! WTF?! Who changed this?
А причина в void f(T& a);
, а должно быть void f(const T& a);
или void f(T* a);
. Функция f()
почему-то изменила значение «а», а писал я ее давно и успел забыть такую ее «особенность». Но из кода f(a)
сходу не видно – может эта функция изменить а
или нет.
А как могло произойти, что мне вообще пришло в голову сделать параметр a
неконстантной ссылкой? Лично у меня это случается, когда переменная изначально была внутри функции, и в какой-то момент я решил сделать ее параметром, а менять в коде везде a.
на a->
было просто лень, вот и сделал ссылку, вместо указателя. За что и поплатился, позже.
Кстати, один из аргументов, приводимый людьми, выступающими за неконстантные ссылки –это «писать a.
приятнее и понятнее, чем a->
или *a
». Также ссылка более надежна с точки зрения NULL
(сделать ссылку, указывающую на NULL
конечно можно, но тут уже надо постараться). Тут можно выйти положения так:
void f(T* ptr_a) { assert(ptr_a != NULL); T& a = *ptr_a; ... if (a.foo()) ... }
Небольшой лишний код, но решены обе проблемы: проверка на NULL
и необходимость разыменовывать указатель каждый раз. А главное, в вызывающем коде придется писать так: f(&a)
, что явно укажет на факт возможной модификации аргумента внутри функции.
Например, в C# есть специальной ключевое слово ref
, которое надо ставить перед аргументами в вызывающем коде, если хочется передать что-то по ссылке. По мне,это очень хорошее свойство языка.
Исключения из правила
Я пока выработал для себя одно исключение: сложные, сервисные прокси-объекты, типа потока или базы данных можно передавать по неконстантной ссылке. Но тут надо четко понимать, не вы меняете этот объект внутри функции, а он меняется сам при обращении к нему.
Например:
void print(std::ostream& os, const T& a) { os << a; }
или
void save_to_db(Database& db, ...) { db.connect(); db.save(...) ... }
Ну и чтобы два раза не вставать, пара личных маленьких радостей:
Лично я только совершенно недавно открыл для себя этот способ постижения технической информации. И пока у меня сугубо положительные ощущения – скорость и эффективность восприятия гораздо быстрее, чем при простом чтении.
Например, посмотрев три видео Coding in Objective-C 2.0, я могу написать небольшую программу на этом языке с использованием классов и имею представление, как работает управление памятью в Objective-C (кстати, весьма интересная концепция, я бы ее назвал – ручной сборщик мусора, когда память освобождается вроде как автоматически, но ответственность за правильность подсчета активных ссылок на объект возлагается на программиста).
Сейчас в процессе просмотра Writing Your First iPhone Application и на очереди Erlang in Practice.
Но есть со скринкастами одна засада – они должны быть качественно сделаны, иначе будет только сожаление о потерянном времени. Скринкасты от The Pragmatic Bookshelf сделаны, на мой взгляд, очень хорошо.
А какие скринкасты смотрите вы? Какие можете посоветовать?
P.S. Кстати, онлайн-магазин «The Pragmatic Bookshelf» нравится мне все больше и больше. Все книги продаются в электронном свободном варианте, причем в нескольких форматах (pdf, mobi, epub).
]]>1. Чисто плюсовый вариант, основанный на семантике ссылок.
class Foo { Value field_; public: Value& field() { return field_; } const Value& field() const { return field_; } };
Использование:
Foo foo; foo.field() = field_instance; field_instance = foo.field();
Плюсы: краткость текста, близость к нотации свойств, и возможность использования присвоения в каскаде (foo1.field() = foo2.field() = 2;
).
Минусы: использование синтаксиса вызова функции слева от знака присваивания выглядит непривычно.
2. Способ в стиле Java.
class Foo { Value field_; public: void setField(const Value& value) { field_ = value; } const Value& getField() const { return field_; } };
Использование:
Foo foo; foo.setField(field_instance); field_instance = foo.getField();
Плюсы: ясность и очевидность синтаксиса.
Минусы: многословность из приставок “get” и “set”.
3. Cтиль Objective-C
class Foo { Value field_; public: void setField(const Value& value) { field_ = value; } const Value& field() const { return field_; } };
Использование:
Foo foo; foo.setField(field_instance); field_instance = foo.field();
Плюсы: краткость текста при чтении (нет почти бессмысленной приставки “get”) и очевидность при записи.
Минусы: я пока не нашел таковых.
Понятно, что все три способа имеют право жизнь. Но с точки зрения стилистики, стоит выбрать один и придерживаться его в рамках одного проекта.
Лично я почти всегда предпочитал способ #1, но после своего последнего поста я перешел на третий.
]]>#include <iostream> struct A { A(); const int& i; }; A::A() : i(123) {} int main() { A a; std::cout << a.i << std::endl; }
и
#include <iostream> struct A { A() : i(123) {} const int& i; }; int main() { A a; std::cout << a.i << std::endl; }
Будучи скомпилированными компилятором от Sun или GCC, эти два примера печатают разные результаты. И первый - неправильный. Студия же 2010, с настройками по умолчанию, дает предупреждение и генерирует код, работающий правильно (точнее, как ожидает программист) в обоих случаях.
Понятно, что код сам по себе несколько странный, так как сложно представить себе, кому может понадобиться инициализировать ссылку константой, которая передается в конструктор не извне, а создается временно во время обработки списка инициализации. Но вот на ошибку-опечатку вполне себе потянет.
Ко мне пример попал как результат анализа реального бага.
]]>const std::string& id() const { return id_; } std::string id() { return id_; }
В нем есть одна досадная опечатка, из которой код:
order.id() = 123;
делал то, что от него не ожидалось, а точнее, ничего не делал. И проблема, как вы наверное уже догадались, в пропущеном значке &
во второй строке. Должно было быть так:
const std::string& id() const { return id_; } std::string& id() { return id_; }
Эта опечатка стоила мне часа поиска проблемы через вторичные признаки в виде иногда не обновляемой базы данных.
Причина? А все потому, что я поленился написать тесты изначально, решив, что это уж очень простые методы. Но теперь таки добавил для этого тест:
TEST(Order, GetterSetters) { Order order; ... EXPECT_EQ(0, order.id()); // Must be initialized. order.id() = 123; EXPECT_EQ(123, order.id()); ... }
Решил сэкономить время, а вышло наоборот.
Вывод: тесты, тесты и тесты.
]]>Надеюсь, почта России одумается и сделает нормальный сервис доставки.
В Лондоне мой любимый книжный - это Foyles на Charing Cross. Ни в Waterstones, ни в Borders не видел столь огромного отдела технической литературы, которая еще и великолепно отсортирована и разобрана по разделам. Например, отдельный шкаф с книгами чисто по компиляторам, или чисто по языку Хаскель, или по QA тестированию, а вдоль стендов про Java или C++ вообще можно ехать на самокате.
В общем, Амазон амазоном, но здорово неспешно зависнуть в таком магазине пару часиков, полистать, пошуршать, пощупать, подумать. Хотя потом можно выписать нужное на бумажку и купить онлайн, ибо дешевле, или скачать.
Куда я первым делом отправился во время визита в Москву на прошлой неделе? В “Библио-Глобус” на Лубянке. Кто знает в Москве офлайновый магазин с большим выбором и лучшей организацией стендов - поделитесь.
Приятно отменить, что очень много переводных книг, причем весьма свежих.
Но еще более приятно, что наша литература ничем не хуже.
В разделе компьютерном купил:
С. М. Окулов, Алгоритмы обработки строк
Как написано в предисловии - это книга посвящена одному вопросу - как искать подстроку в строке за линейное время. На первых страницах рассматривается Кнут-Моррис-Пратт, а дальше начинается всякая жесть. Забавно, что книга из серии “Развитие интеллекта школьников”, то есть для просто школьников.
Просто отличная форма изложения: с картинками, диаграммами, пояснениями и примерами. Одна грамотная иллюстрация бывает лучше тысячи слов. Я видел несколько книг Окулова - могу только обоими руками поддержать этого автора. Писать для школьников о сложных алгоритмах - это трудно. А делать еще и понятным языком - особенно.
Вывод: иметь.
Далее, в соседнем разделе математики купил:
С. К. Ландо, Лекции о производящих функциях
Тут прямо на первых страницах этой методички понятнейшим образом объясняется, что такое производящие функции и их применение для решения комбинаторных задач, причем для понимания даже не обязательно вспоминать Анализ. Разбираются классические задачи про правильные скобочные последовательности, числа Каталана, язык Дика и многие другие. Местами там, конечно, жестковато с точки зрения математики, но вводные даются просто великолепно.
Эта книга есть в свободном варианте.
Ну и чтобы два раза не вставать, под занавес две ссылки на грамотные подборки электронных книг.
http://www.mccme.ru/free-books/ http://e-maxx.ru/bookz/
P.S. Кто знает еще хорошие ссылки по электронным книгам - делитесь в комментах.
]]>#include <stdio.h> #include <math.h> int main() { int i; for (i = 0; i <= 1; ++i) { float a = (i ? floor : ceil) (10.5); printf("%d: %f\n", i, a); } return 0; }
Для С++ надо написать:
#include <stdio.h> #include <cmath> int main() { int i; typedef float (*f)(float); for (i = 0; i <= 1; ++i) { float a = (i ? (f)std::floor : (f)std::ceil) (10.5); printf("%d: %f\n", i, a); } return 0; }
или
#include <stdio.h> #include <cmath> int main() { int i; for (i = 0; i <= 1; ++i) { float a = (i ? std::floorl : std::ceill) (10.5); printf("%d: %f\n", i, a); } return 0; }
Все программы выводят:
0: 11.000000
1: 10.000000
]]>Я давно коллекционирую интересные мне книги в электронном виде (pdf, djvu, mobi, epub и т.д.), но они просто лежали, так как читать с экрана в удовольствие я не могу, поэтому я обычно либо печатал на бумагу, или таки покупал бумажный экземпляр.
И вот все изменилось. Чтение накопленных запасов книг получило новый виток - iPad.
Форм-фактор, свечение экрана, время работы (2-3 дня при чтении 2-3 часа в день плюс серфинг через Wifi или 3G), и главное - программы для чтения просто приковывают к этому устройству.
У меня нет особых сантиментов на тему ностальгии по бумажным книгам. Для меня главное - двух-кликовая доступность практически любой книги из моей коллекции в любое время, не вставая с дивана, и комфорт чтения. Техническая, а особенно справочная литература вообще умерла в бумажном виде из-за отсутствия ключевой функции - быстрого поиска.
Да и с художественной у меня нет особых причин не читать на айпаде. Первым делом я заправил туда подборочку любимого Рампо Эдогавы.
В общем, теперь у меня с собой весь свой шкаф книг, почти. Причем, с закладками, с поиском, личными пометками, переведенными словами и т.д.
Если это нормальный файл-исходник, а не пиксельный скан, то можно спокойно пользоваться стандартным приложением iBook и закачивать туда книги через iTunes. Очень грамотная программа для чтения (i-все-таки). Но если приходится иметь дело с pdf’ками, которые по сути являются постраничными сканами (таких “книг” обычно большинство на просторах торрентов), то тут бесспорный лидер - это Good Reader. Очень быстро работает даже с тяжелыми файлами, умеет сам выкачивать файлы из интернета (HTTP, Dropbox, Google Docs, POP3/IMAP и т.д.), и самое главное - умеет делать конфигурируемую “обрезку” страниц. Это о-о-очень полезно. Часто книги отсканированы через пень-колоду, и например, в порядке вещей наличие гигантских отступов-полей, которые могут отжирать и так ограниченное пространство зоны чтения. А “обрезкой” можно персонально для каждой книги (для четных и нечетных страниц также) задать отображаемую часть листа. Вывод: Good Reader - это номер 1.
DjVu
К сожалению, Good Reader не понимает этот формат, а те программы, которые понимают, кривые. Поэтому я просто печатаю DjVu в PDF через PDFCreator и уже PDF заправляю на айпад.
Где брать книги? Конечно, торренты. Тут не надо ничего объяснять.
Отдельно расскажу по технологию Kindle. Это амазоновская читалка. Но есть еще и приложения Kindle для iPhone/iPad/iPod, Android, Windows Mobile, PC и т.д. В чем тут цимес? Для начала - время покупки электронных Kindle-книг на Амазоне исчисляется минутами: выбрал, оплатил и через пару минут книга сама будет закачена на устройство (можно сначала бесплатно скачать preview в виде десятка первых страниц).
Сами книги сделаны очень добротно, с хорошей и удобной версткой. Можно делать закладки, пометки в тексте, а при клике на слово вызывается оксфордовский толковый словарь (жаль, конечно, что не русский подстрочник). Также можно искать по тексту. А теперь ключевая фича - это синхронизация между различными устройствами. Допустим, у меня зарегистрированы читалки: программа-Kindle на PC, на айпаде и на Nexus’е. Все, что делаю с книгой на любой из читалок (ставлю закладки, пометки, позиция чтения) автоматически синхронизируется на другими устройствами.
Теперь я всегда на Амазоне сначала смотрю наличие электронной книги и покупаю ее, если есть (кстати, при покупке электронной версии не должно более быть проблем с доставкой, например, в Россию). Жалко, что пока ассортимент книг несравнимо мал, по сравнению с полным каталогом.
Есть с амазоновскими книгами одно “но”. Файлы книг как таковые как бы пользователю не даются. Они видны во всех зарегистрированных читалках, и их можно скачивать повторно, но вытащить напрямую “официально” не получается.
Я написал письмо в Амазон:
Dear Sirs,
I’ve purchased a few books in Kindle format via Kindle application on iPad and Nexus. I would like to have those books as files (PDF, mobi, ebook etc). How can I get my purchased items this way?
Thanks, Alexander
Получил ответ:
Hello Alexander,
Content you purchase from the Kindle Store (such as books, newspapers, magazines and blogs) can be read on most Kindle devices or devices running a Kindle application registered to your Amazon.co.uk account. Information about devices that can read Kindle content can be found in our Help pages here:
http://www.amazon.co.uk/kindlesupport
Kindle Store content is not currently viewable on any other electronic reading devices. We don’t offer Kindle content in any other formats so you would have to see if they are available elsewhere in different formats to read outside Kindle applications.
I hope this helps.
Значит типа “хрен тебе”.
Ясное дело, проще всего покопаться с программой на PC. Файлы с книгами я нашел быстро. Это обычный mobi-формат, но, конечно, зашифрованный. Но если книга читается, когда интернет отключен, значит ключ где-то на самом компьютере, а значит - его можно найти. На просторах интернета нашлась волшебная программа skindle, которая в момент из зашифрованной DRM-ленной книги делает просто mobi-книгу.
И дело не в том, что я мог бы выложить книги в сеть. Я этого делать не буду, так как я за них денег платил, но сам факт ограничения доступа к файлу - это глупость. Хотя надо отметить, некоторые издательства, например “The Pragmatic Bookshelf” уже продают книги в электронных форматах. При это тебя не ограничивают во владении файлом, делай с ним что угодно. Просто каждая страница в колонтитуле мелким шрифтом содержит твое имя, взятое из данных платежа. То есть файл типа персонализирован. Понятно, это все можно тоже вырезать, но пусть этим занимаются те, кому охота.
Конечно, айпад - это весьма дорогая читалка книг, есть альтернативы, но сам подход электронных книг - это какой-то нереальный скачок вперед.
Жаль, что почти нет изданий на русском языке, которые продавались бы в нормальном электронном виде, а не были бы уродливыми сканами с бумажных копий.
UPDATE: Совсем забыл. Для чтения книг в CHM-формате тоже есть хорошее приложение для айпада.
]]>Вот оригинал на C++ (рекурсия с кешированием):
bignum f(int n, int k, int t, int& K, vector<vector<vector<bignum> > >& dp) { if (n == 0 && k == 0 && t == 0) return 1; if (n == 0 || k < 0) return 0; if (!(dp[t][n][k] == -1)) return dp[t][n][k]; bignum ans = 0; if (k < K) { ans = ans + f(n - 1, k - 1, t, K, dp); if (t == 1 || (t == 0 && k < K - 1)) ans = ans + f(n - 1, k + 1, t, K, dp); } if (t == 1 && k == K) { ans = ans + f(n - 1, k - 1, 0, K, dp); ans = ans + f(n - 1, k - 1, 1, K, dp); } dp[t][n][k] = ans; return ans; }
А теперь Эрланг:
main(_) -> N = 50, K = 25, io:format("~w\n", [rbs(2*N, 0, 1, K)]). rbs(0, 0, 0, _) -> 1; rbs(0, _, _, _) -> 0; rbs(_, K, _, _) when K < 0 -> 0; rbs(N, K, 0, MAX_K) -> if K < MAX_K-1 -> rbs(N-1, K-1, 0, MAX_K) + rbs(N-1, K+1, 0, MAX_K); K < MAX_K -> rbs(N-1, K-1, 0, MAX_K); K =:= MAX_K -> 0 end; rbs(N, K, 1, MAX_K) -> if K < MAX_K -> rbs(N-1, K-1, 1, MAX_K) + rbs(N-1, K+1, 1, MAX_K); K =:= MAX_K -> rbs(N-1, K-1, 0, MAX_K) + rbs(N-1, K-1, 1, MAX_K) end.
Красиво, а?
Но вот увы, кеширование результатов пришлось делать тоже вручную, так как без нее программа имеет экспоненциальное время. Так что итоговый вариант выглядит так:
#!/usr/bin/env escript main(_) -> N = 50, K = 25, io:format("~w\n", [rbs(2*N, 0, 1, K)]). rbs(0, 0, 0, _) -> 1; rbs(0, _, _, _) -> 0; rbs(_, K, _, _) when K < 0 -> 0; rbs(N, K, 0, MAX_K) -> if K < MAX_K-1 -> rbs_memo(N-1, K-1, 0, MAX_K) + rbs_memo(N-1, K+1, 0, MAX_K); K < MAX_K -> rbs_memo(N-1, K-1, 0, MAX_K); K =:= MAX_K -> 0 end; rbs(N, K, 1, MAX_K) -> if K < MAX_K -> rbs_memo(N-1, K-1, 1, MAX_K) + rbs_memo(N-1, K+1, 1, MAX_K); K =:= MAX_K -> rbs_memo(N-1, K-1, 0, MAX_K) + rbs_memo(N-1, K-1, 1, MAX_K) end. rbs_memo(N, K, T, MAX_K) -> Name = rbs, case ets:info(Name) of undefined -> ets:new(Name, [public, named_table]); _ -> true end, Key = {N, K, T, MAX_K}, case ets:lookup(Name, Key) of [] -> Val = rbs(N, K, T, MAX_K), ets:insert(Name, {Key, Val}), Val; [{_, Val}] -> Val; Else -> Else end.
Странно, ведь можно было бы заложить кеширование прямо в языке. Ведь когда функция вызывается повторно с теми же самыми аргументами, то очевидно, что результат будет такой же (ибо Эрланг - это “чистый” функциональный язык, и side effects тут исключены). Но в целом функция кеширования весьма универсальна и ее можно быстро адаптировать для других ситуаций.
По времени выполнения эрланговый вариант не уступает C++. При это надо учесть, что Эрланге по умочанию арифметика “длинная”, а в С++ мне пришлось использовать доморощенный класс bignum
.
Один из конкурсов, от которого меня реально вставляет – это Realtime Chip Hack.
Вкратце суть: вам дается тестер, пара проводков и светодиодов. С помощью данного нехитрого набора требуется в некоторой реальной работающей схеме определить тип микросхемы, маркировка которой спилена. Еще как вариант – дается устройство-индикатор обратного отсчета (типа бомба). И надо с таким же набором юного сапера ее обезвредить (перерезать дорожку, или наоборот добавить проводок). Но, например, неверное действие может ускорить «бомбу» или просто сразу ее взорвать.
Я однозначно нахожу это крайне близкой темой к reverse-engineer’ингу, где надо просто взять и разобраться, как программа, например, проверяет ключ, и написать ключелку или битхак.
И прелесть тут в том, что все, что отделяет тебя от победы – это не мегабайты кода, а всего 1-2 байтика (или проводочка), которые найти и исправить.
]]>На этот раз это сайт hackquest.com. Не знаю, сколько он уже существует, но я только сейчас о нем узнал.
Итак, это сборник разнообразных головоломок на около компьютерные тематики: логика, JavaScript, Java, reverse engineering, криптография, интернет, взломы, Flash, программирование и т.д.
Я вчера посидел пару часов и порешал задачки из своей любимой темы reverse engineering’а. Получил удовольствие.
Рекомендую.
]]>В общем, я становлюсь фанатом этого дядьки.
Update: А комментарий с семью различными типами умных указателей в Бусте - это вообще шедевр.
]]>Update: У нас тут есть свои блумберговские кубики, на грани которых кроме цвета нанесена корпоративная атрибутика. Подобное “улучшение” сильно осложняет сборку, так как необходимо правильно сориентировать остов до начала сборки слоев. Причем данная ориентация может съехать при неосторожной попытке сборки начального слоя.
]]>#include <iostream> int x; struct A { A(int a) { x = a; } }; struct B { B(A a) { A local_a = a; } }; int main() { x = 0; std::cout << "Case #0: " << x << std::endl; B b1(A(1)); std::cout << "Case #1: " << x << std::endl; int t; t = 2; B b2(A(t)); std::cout << "Case #2: " << x << std::endl; t = 3; B b3((A(t))); std::cout << "Case #3: " << x << std::endl; return 0; }
Как вы думаете, что должна вывести эта программа? Числа 0, 1, 2 и 3 последовательно для каждого случая?
А она печатает:
Case #0: 0
Case #1: 1
Case #2: 1
Case #3: 3
Почему для случая #2 не произошло присваивание? Куда делась двойка?
Ответ на этот вопрос кроется в наличии рудиментов языка С в грамматике С++.
]]>Один из частых вопросов, которые задают у нас в Блумберге на интервью в плане так называемого coding exercise (это когда надо на бумаге или на доске написать почти реальный кусок кода) - это поиск строки в подстроке. Это абсолютно жизненная задача.
Подавляющее число людей пишут первое, что приходит в голову - это два вложенных цикла, когда искомая строка последовательно “прикладывается” с каждой позиции исходной строки. Такой подход дает сложность O(M*N)
, и, очевидно, что в жизни он нереален, если строки более менее длинные. Хотя все при этом знают, что любой hex-вьювер спокойно ищет в огромных файлах за явно линейное время.
Итак, для эффективного поиска строки в подстроке есть алгоритм Кнута-Морриса-Пратта, который решает проблему не за O(N*M)
, а за O(N+M)
.
Построенный на префикс-функции, данный алгоритм является классичесим примером динамического программирования, когда результаты решения задачи малой размерности используются для решения задачи большей размерности.
P.S. Сразу скажу, у нас никто не просит по памяти писать КМП. И если человек после написания простого O(N^2)
решения также скажет, что есть метод быстрее, и сможет описать его идею - этого вполне достаточно.
#include <iostream> volatile const char* p = "0"; int main() { std::cout << p << std::endl; return 0; }
Для получение схожего эффекта в GCC надо заменить “0” на “false”.
]]>В этом блоге я буду публиковать мои личные субъективные заметки на различные аспекты жизни в Великобритании, начиная от штучек английского языка и заканчивая местными прелестями или маразмами, перемешанными с особенностями национального характера.
Некоторые заметки были сделаны уже давно, но я планирую также публиковать новое по ходу поступления. Источники - люди, работа, газеты, телевизор, и, конечно, жизнь вокруг.
Все будет рассматриваться, естественно, через призму оцифрованного мозга программиста, поэтому часть постов будет определенно про профессию программиста в Великобритании.
]]>Будем говорить, что число a лучше числа b, если сумма цифр a больше суммы цифр числа b, а в случае равенства сумм их цифр, если число a меньше числа b. Например, число 124 лучше числа 123, так как у первого из них сумма цифр равна семи, а у второго – шести. Также, число 3 лучше числа 111, так как у них равны суммы цифр, но первое из них меньше.
Дано число n (1 ≤ n ≤ 10^5). Найдите такой его делитель (само число n и единица считаются делителями числа n), который лучше любого другого делителя числа n.
Простая задача: ищем все делители n, у каждого делителя считаем сумму цифр, и среди найденного выделяем все делители с минимальной суммой, и уже среди них находим минимальное по значению. Просто надо аккуратно запрограммировать.
Я все сделал, и решение прошло.
Далее была вторая задача:
Будем говорить, что число a лучше числа b, если сумма цифр a больше суммы цифр числа b, а в случае равенства сумм их цифр, если число a меньше числа b. Например, число 124 лучше числа 123, так как у первого из них сумма цифр равна семи, а у второго — шести. Также, число 3 лучше числа 111, так как у них равны суммы цифр, но первое из них меньше.
Дано число n (1 ≤ n ≤ 10^5000). Найдите такой его делитель d (само число n и единица считаются делителями числа n), что любой другой делитель c числа n лучше, чем d.
По сути, такая же задача, что и предыдущая – просто нужно в паре мест поменять «больше» на «меньше» и наоборот. Я поменял, и на небольших тестах решение прошло.
Но тут я замечаю, что в это задаче верхнее ограничение на n какое-то очень большое. Я заменяют тип int
на класс «длинной» арифметики (5000 тысяч знаков – пустяк, было и более) и пробую: на малых тестах работает, значит, алгоритм верный.
Посылаю решение, и конечно, получаю ответ «Time limit exceeded». После небольшого раздумья становится ясно, что слишком много надо считать, и подход в целом неправильный.
Пишу письмо Leonid’у с просьбой прояснить ситуацию.
Приходит ответ типа, что моя попытка, ища все делители громадного числа, взломать RSA брутборсом конечно похвальная, но исполнена жиденько, да и врядли она вообще под силу современным компьютерам. К тому я же ищу все делители, а не только простые, а их будет еще больше.
В итоге выяснилось, что данная задача решается не только без «длинной» арифметики, но даже и без короткой, одним циклом в пару строк. Просто надо понять, что за тип чисел просят найти.
]]>... class A { virtual void f() {} }; class B {}; int main() { A a; try { B& b = dynamic_cast<B&>(a); if (&b == 0) { // ... } } catch (...) { std::cout << "Got it!" << std::endl; } return 0; }
Я слегка выпал в осадок от уведенного, а в частности, от строки if (&b == 0) {
. До сего времени я пребывал в осознании факта, что ссылка в С++ либо существует и указывает на реальный объект, либо ее нет вообще. И если тут приведение типа к B&
не срабатывает, то будет исключение, и управление все равно улетит в другое место, и проверять как-либо b
бессмысленно.
Но тут мне объяснили, что в данном конкретном случае код может компилироваться, когда у компилятора выключена поддержка исключений. И эта проверка как раз защита от этого.
Ну да ладно. Оставим это на откуп странным компиляторам на AIX и SUN, и людям, использующим исключения, но почему-то компилирующие с принудительным их выключением в компиляторе.
Меня заинтересовал другой вопрос: как вообще ссылка может существовать отдельно от объекта. Оказывается, может:
int& a = *(int*)0; int main() { a = 123; }
Данный код прекрасно компилируется Студией (2010) и компилятором SUN (этот хоть предупреждение выдает), и также прекрасно падается при запуске по понятой причине.
Вы получили ссылку в качестве параметра и думаете, что она лучше чем указатель, так как ее не надо проверять на NULL
? Зря!
В частности, курс Introduction to Algorithms.
Все сделано максимально удобно: в добавок к видео (некоторые идут сразу с субтитрами) еще есть и транскрипт каждой лекции.
Я еще рекомендую заценить примеры экзаменационных задач.
Честно говоря, берет зависть, насколько там все удобно сделано для учебы.
]]>Например, вместо:
void f(T& t) { // change ‘t’ } ... T a; f(a);
я предпочту передачу по указателю:
void f(T* t) { // change ‘*t’ } ... T a; f(&a);
Мой основной мотив – наглядность в вызывающем коде. Когда я вижу &
перед аргументом, я точно знаю, что это возвращаемый параметр, и он может измениться после вызова. И мне не надо постоянно помнить, какие именно агрументы у этой функции ссылки, а какие нет.
Конечно тут есть и минусы: корявость текста в вызываемом коде, так как надо таскать за собой *
или ->
. Также неплохо бы проверять указатель на ноль.
А у вас есть предпочтения в этом вопросе?
]]>В одном месте собрано большое количество макросов для определения компилятора, библиотеки, операционной системы, архитектуры и стандарта языка.
]]>Например, можно зайти в наш раздел вакансий, указать в «Job Function» категорию «Research and Development» (R&D) и получить список открытых позиций для программистов во всех наших офисах.
Я работаю в Лондонском офисе, и поэтому многое из этого поста относится к лондонскому R&D, но в целом, все верно и для остальных офисов.
Например, конкретные вакансии в подразделение «Trading Systems», где работаю я: раз, два и три.
Что важно, по многим вакансиям (например, нашим) не требуют специальных знаний из области финансов, трейдинга и т.д. Если они есть – замечательно, но в целом мы просто ищем грамотных инженеров, или по-русски говоря, программистов.
Подавляющее количество позиций – это С++ и UNIX, но, например, в мобильную команду (iPhone/iPad/Android/Blackberry) также приглашают специалистов по Objective-C и Java.
Я координирую процесс найма в нашу группу, поэтому знаю процесс чуть более глубоко, и могу подсказать детали интересующимся.
Как подать резюме? Нет ничего проще – просто послать его через сайт. Многие думают, что такие заявки никто не читает – это неверно. Я сам устроился точно таким же образом. Все читают. Другое дело, часть отсеивается на стадии начального анализа присланных резюме, хотя, я думаю, что на этой стадии отсеивают только полностью неадекватные заявки, когда люди даже не пытаются прочитать требования и хоть как-то отразить это в своем резюме.
Какие шансы пройти? Ничего гарантировать нельзя, но если у вас 4.0 и более баллов на Brainbench на тесте по С++ (не забудьте дать ссылку на профайл в резюме) и более чем пятилетний постоянный опыт больших проектов на С++ , то шанс добраться до телефонного интервью весьма велик. А если вдобавок у вас есть рейтинг на TopCoder’e, CodeForces или что-то в этом роде (также не забудьте дать ссылку), то шансы и на личном интервью очень даже неплохие.
Теперь самая интересная часть марлезонского балета – а как это все работает для граждан России и бывших соцреспублик, у которых нет европаспорта? Могу сказать, что Блумберг может предоставить визовое спонсорство для понравившегося кандидата.
Не надо бояться. Даже телефонное интервью – это хороший опыт. Те, кто никогда этого не делал на английском, но хочет – однозначно стоит попробовать.
Зарплаты? Блумберг всегда адекватен к текущему уровне зарплат на рынке. Насколько я знаю, сейчас зарплаты программистов в Лондоне могут колебаться от 35K до 75K фунтов в год (50K-100K USD) минус 20% налогов Великобритании. Вообще, для ориентации по заплатам и вакансиям можно посетить jobserve.co.uk или monster.co.uk.
А как вообще работается в Блумберге? Интересно (объемы внутренних технологий объять невозможно), динамично (недельные релизы прямиком в онлайновый продакш), разнообразно (тренинги, возможность не уходить в менеджмент, если не хочется), престижно.
Ну и под занавес, как говорится, чтобы два раза не вставать – у нас в Лондонском офисе 22-го июля будет Bloomberg Technology Open Evening – день открытых дверей для разработчиков. Однозначно понимаю, что не все россияне могут так чисто взять и на денек ради этого сюда приехать, на всякий случай – вдруг кому будет-таки полезно, и кто захочет прийти – могу сделать инвайт (пишите на alexander@demin.ws).
]]>Живая карта Лондонского метро - http://traintimes.org.uk/map/tube/
Тут видно движение отдельных поездов.
И опять-таки, идея нереволюционная. Но! Самое важное, что такой сервис есть благодаря public API, официально поддерживаемый Лондонским Метро, которая является полугосударственной структурой. Теперь ясно, откуда такое изобилие приложений для айфона и андроида, предоставляющих разнообразную on-line информацию о метро. Я то думал, они просто парсят страницы официального сайт, а тут, оказывает, все дается на тарелочке.
]]>Я уже долгое время тщетно пытаюсь это делать. Но дело как-то не идет. Каждый пост занимает в разы больше времени, посты приходится укорачивать, упрощать язык (“эдак” уже не завернешь). А самое, что внутренне неприятно - я четко вижу разницу в качестве языка. Вместо более-менее живого и, надеюсь, интересного, способа выражения мыслей, что я себе могу позволить на русском (хех, все-таки 30 лет практики), на английском получается какое-то сухое школькое сочинение, часто с неочевидными ошибками. И уж точно выглядещее, как НЕклево..
Кстати, а вы знаете интересные блоги на английском (хотя бы технические), которые пишут носители русского? (конечно, если человек вырос в английской среде с 5-6 лет, то это не в счет).
]]>А вот к чему. Если разобраться, что я могу писать код вообще без С-шных штучек типа массивов, malloc/free, старого способа приведения типов, строчек с нулем на конце и т.д. Получается, что мой диалект С++ можно урезать на тему всего, что я перечислил. Просто убрать, и все.
И от этого будет только польза. Сколько ошибок потенциально я НЕ сделаю в арифметике указателей (ее просто не будет)? Вместо того, чтобы мотивировать человека “разумно использовать наследия С в С++”, их надо просто выключить. Конечно, сразу сузится и круг решаемых задач, но мой, кстати, не самый узкий круг, можно покрыть таким вот урезанным в сторону “правильного” С++ диалектом С++.
Например, как мне кажется, функция memset()
в мире С++ в целом годится разве что для обнуления. Использование какой-либо иной константы-заполнителя принципиально приближает нас к проблемам с памятью. Хотите, например, “эффективно” заполнить строку пробелами, и зарядите для этого memset()
, а потом вам неожиданно придется работать с многобайтовыми кодировками, и этот пробел, записанный через memset()
, может стать источником проблем.
Так что используйте алгоритм fill_n()
вместо memset()
. Может быть неэффективен? Может, а может и нет. Зато уж точно безопасен с точки зрения типизации.
Y=D(X)
, где X
- натуральное число, а D(X)
- сумма цифр его десятичной записи. Оказывается, вот такой хитрой формулой:
=SUMPRODUCT(MID(A1,ROW(INDIRECT("1:" & LEN(A1))),1))
можно в Экселе посчитать этот самый D(X)
. Ума не приложу, что это формула значит, но точно работает.
А вот и сам график, кому интересно:
]]>http://code.google.com/events/io/2010/sessions.html
Мне, например, очень понравилась презентация про новую Java машину с JIT в Android 2.2:
И про язык программирования Go:
]]>Задача номер 1.
Есть набор куриц и коров. У каждой курицы две ноги и одна голова. У коровы четыре ноги и одна голова. Даны два числа: количество ног и количество голов (каждое число от 1 до 10^6). Надо ответить на вопрос: возможно ли существования некоторого количества куриц и коров при заданном количестве ног и голов.
Даже после небольшого раздумья ясно, что тут этого просто система двух линейных уравнений, которая решается просто подстановкой одного в другое. Далее надо проверить найденные количество куриц и коров на правильность: неотрицательность, целочисленноть и т.д.
Все просто. Но! В два часа ночи я родил иной подход: перебор! (для системы-то из двух линейных уравнений). Я даже помню ход свох мыслей: так как количество только 10^6, значит можно перебрать все возможные значение количество куриц. Для каждого значения вычислить количество коров, исходя их количество ног и из количество голов. Полученные значения сравнить, и если совпадают, значит найдено возможное решение.
Вот такой вот «мега»-подход. В ходе челленджа я просмотрел решения всех участников в комнате, но такого «оригинального» решения не видел. Представляю круглые глаза тех, что пытался зачеленджить мое решение, когда они видели решение двух линейных уравнений перебором.
Кстати, решение в итоге не прошло системных тестов. И к лучшему.
Задача номер 2.
Дано неограниченное поле, в некоторой точке которого стоит робот. Дана программа для робота – строка, каждый символ которой – команда: «S» - шаг вперед на какую-то константную величину, «L» - поворот налево, «R» - поворот направо. Требуется сказать, будет ли траектория робота, двигающего по этой программе, ограничена конечной окружностью, или робот будет постоянно удалятся от исходной точки. Длина программы для робота до 250 символов.
Как один из вариантов решения: можно прогнать 4 итерации программы, и если хотя б после одной, робот вернется в исходную точку, то траектория робота ограничена.
Решение есть. Теперь его надо запрограммировать. Надо было сделать имитацию выполнения программы, то есть команды робота «вперед», «влево» и «вправо».
Приведу фрагмент программы, которую родил мой воспаленный мозг ночью:
if (cmd == 'S') { x += dx; y += dy; } else { if (cmd == 'L' && dx == 0 && dy == 1) dx = -1, dy = 0; else if (cmd == 'R' && dx == 0 && dy == 1) dx = +1, dy = 0; else if (cmd == 'L' && dx == 1 && dy == 0) dx = 0, dy = 1; else if (cmd == 'R' && dx == 1 && dy == 0) dx = 0, dy = -1; else if (cmd == 'L' && dx == 0 && dy == -1) dx = 1, dy = 0; else if (cmd == 'R' && dx == 0 && dy == -1) dx = -1, dy = 0; else if (cmd == 'L' && dx == -1 && dy == 0) dx = 0, dy = -1; else if (cmd == 'R' && dx == -1 && dy == 0) dx = 0, dy = 1; ...
Даже сейчас я не могу смотреть на это без содрогания. Надо то было всего:
... int dx[4] = { 0, 1, 0, -1 }; int dy[4] = { 1, 0, -1, 0 }; int d = 0; ... if (cmd == ‘S’) { x += dx[d]; y += dy[d]; } if (cmd == ‘L’) d = (d + 4 - 1) % 4; else if (cmd == ‘R’) d = (d + 1) % 4; ...
И все!
Но увы. Когда время ограничено, на ум приходят странные решения.
Хотя мое решение прошло системные тесты, от его вида как-то грустно.
Задача 3.
Дано определение числа Y
: Y = X/D(X)
. X
– натуральное число, D(X)
– сумма цифр в десятичной записи числа Х
. Если Y
получается целым, то читается, то Y
есть «предок» числа X. Дан интевал чисел от а
до b
(каждое от 0 до 10^9), но a-b
всегда не более 10000.
Спрашивается, сколько есть на данном интервале есть числе Y
, у которых нет предка (то есть нет такого Х
, чтобы Y
был целым).
Итак, для решение перебираем все значения Y
от а
до b
(их не более 10000). Для каждого Y
числа надо проверить, если ли у него «предок». Ясно, что заменатель D(X)
принимает только значения от 1 до 81 (число с максимальной суммой цифр – это 999999999, а их сумма 81). Итак, делаем внутренний цикл от 1 до 81, и вычисляем X
как X=Y*i
(i
от 1 до 81). Далее по факту равенста i
и D(X)
(надо вычислить сумму цифр, составляющих текущее X
) можно понять, если ли у текущего Y
предок или нет.
Всего итераций будет максимум 10^4 * 81 * 9
~= 10^6
= 1000000, что вполне реально.
Но эту задачу я засабмитить уже не успел, хотя ее решение у меня было в голове.
Итак, выводы.
А вывод простой – работая под жестким временным ограничением, порой самые очевидные вещи вылетают из головы, не говоря уже об «экзотике», типа хитрых алгоритмов. Но так же вывод – так как решения таки были, то значит есть потенциал и закодить из вовремя.
Заснуть под утро я так уже не смог. Все думал.
]]>v
, и надо перебрать все возможные варианты разделения его компонент на два непересекающихся подмножества.
Если элементов множества немного, а именно - их количество умещается в разрядную сетку вашего компьютера, например, не более 32-х или 64-х, то есть элегантный способ организовать перебор следующим образом:
vector<int> v(20); // Исходное множество // Всего вариантов будет: 2^(v.size())-1. for (int i = 1; i < (1 << v.size()); ++i) { vector<int> left, right; for (int j = 0; j < v.size(); ++j) { if ((i >> j) & 1) left.push_back(v[j]); else right.push_back(v[j]); } // Текущий вариант множеств left и right готов для обработки. ... }]]>
А что в жизни? В жизни-то оно применимо? Конечно!
Итак, разностная машина, построенная из Лего!
Как сказано в описании: “Computing the next entry in a table can be significantly easier than computing an arbitrary entry of the table.” - классика динамического программирования.
Вот оно - настоящее Железо!
Ссылки по теме:
]]>void v() {} void f(){ return v(); } int main() { f(); }]]>
Формула может быть в замкнутом виде, когда ты просто подставляешь аргументы и разом получаешь результат за время O(1)
, или в динамической, рекуррентной форме, когда для вычисления i-го члена надо вычислить предыдующие.
Классический пример замкнутой формы - это сумма ряда n положительных чисел:
S(n)=n*(n-1)/2.
А для динамической, или рекуррентной формы - это, например, формула для i-го члена последовательности чисел Фибонначи:
F(i)=F(i-1)+F(i-2).
Например, вот такая задача (для школьников!).
Есть N чисел от 1 до N. Требуется разместить эти числа в ряд слева направо. Первым числом всегда идет 1. Каждое последующее может отличаться от предыдущего не больше, чем на 2. Например:
1 2 3 4 5 ...
1 3 2 4 5 ...
1 3 5 4 2 ...
и т.д. (эта задача есть на Тимусе).
Требудется найти количество возможных размещений по этому правилу для N чисел. N от 1 до 55.
Просто перебором в лоб не получится, так как вариантов будет 55!
~ 10^73
, а времени дается всего одна секунда.
Судя по ограничению по времени в 1 секунду, тут просто должна быть формула.
А вот теперь подходим к сути поста. Недавно я наткнулся на волшебный сайт - Онлайн-энциклопедия целочисленных последовательностей. Там ты просто вводишь несколько известных тебе членов последовательности, а сайт по большой базе данных ищет совпадения с какими-либо известными последовательностями, и в случае наличия такого совпадения, дает исчерпывающую информацию - формулы, статистический анализ и т.д.
Итак, я нашел пяток первых значений решения задачи перебором - генерировал все возможные перестановки для i членов, для i от 1 до 8, и проверяя для каждой перестановки правильность выполнения условий размещения, считал количество вариантов.
В итоге, магия произошла. Когда я ввел вычисленные первые элементы последовательности в этот анализатор, мне просто сказали, что формула для i-го члена этой последовательсти: a(n)=a(n-1)+a(n-3)+1
. И все! И по данной формуле задача решается пулей одним циклом за O(N)
.
Конечно, для реальных соревнований такое метод не годится, так как никто не даст доступа в интернет. Да и цель задачи несколько иная - научиться выводить такие динамические соотношения самому.
А вот в жизни - бывает, что числа уже есть, и надо быстренько как-то их обсчитать на больших размерностях, вот тут и пригодится этот волшебный анализатор, которые даст исчерпывающуй информацию о ваших числах.
]]>#define T 2 class A { public: virtual ~A() { p = 0; } int p; }; class B: public A { int a; }; int main() { A* a = new B[T]; delete[] a; return 0; }
У меня эта программа однозначно падает с Segmentation fault
на строке delete[] a
. Проверено на Sun C++ на Солярисе, GCC на Линуксе и на FreeBSD. Вот, например, что происходит на BSD:
Program received signal SIGSEGV, Segmentation fault.
0x08048743 in main () at new_array.cpp:17
17 delete[] a;
Забавно, что под Windows в VS2008 ничего особенного не происходит.
Как я понимаю, что в этой программе принципиально важно, чтобы она падала: деструктор класса A
должен быть виртуальным, дочерний класс B
должен быть больше по размеру (тут есть член a
), константа Т
должна быть 2 или более (то есть мы должны создавать несколько экземпляров класса B
), и деструктор класса A
должен что-нибудь писать в свои члены (тут есть p = 0;
).
Что же тут происходит?
new[]
создает массив экземплятор класса B
. Оператор же delete[]
получает на вход указатель типа A*
и начинает вызывать деструкторы элементов. Так как деструктор класса А
виртуальный, то в ход пускается таблица виртуальных функций. Итак, отработал деструктор для первого элемента a[0]
. Далее delete[]
хочет получить адрес следующего элемента массиве a
. И для этого (внимание!) адрес следующего он вычисляется так: a + sizeof(A)
(ему же на вход дали указатель типа A*
). Но проблема в том, что sizeof(A) < sizeof(B)
(это дает член класса B::a
), и a + sizeof(A)
будет указывать не на второй элемент в массиве a
, а куда-то между первым и вторым элементом, так как реальный адрес второго элемента - a + sizeof(B)
. И все бы ничего, но деструктор класс A
пишет в член p
, тем самым меняя содержимое памяти, а так как для второго элемента адрес вычислен неправильно (его this
указывает непонятно куда), то куда реально попадет 0 в присваивании p = 0;
уже никто не знает, но явно не туда, куда надо. Вот и Segmentation fault
.
Другого объяснения у меня нет.
Если кто знает лучше, поправьте.
P.S. Забавно, что под виндами ничего страшного не происходит.
Update: В комментариях дали точное объяснение из стандарта: C++ 2003 5.3.5:
…In the second alternative (delete array), the value of the operand of delete shall be the pointer value which resulted from a previous array new-expression. If not, the behavior is undefined. [Note: this means that the syntax of the delete-expression must match the type of the object allocated by new, not the syntax of the new-expression.]
Update 2: Объяснение, почему не глючит в Visual Studio.
]]>Так как я не нашел, как сделать голосование прямо в теле поста, поэтому вставил голосовалку в правую колонку блога. Так что если вы читаете блог через RSS и, возможно, не ходите на сайт напрямую, но хотите внести свой голос, то просто зайдите на главную страницу блога. Голосовалка прямо там, вверху справа.
]]>Scheduling для двоих детей, особенно с небольшой разницей в возрасте, конкретно отличается от одного, и является гораздо более сложной оптимизационной задачей, ибо количество переменных увеличивается на порядок. Но в целом - переходный процесс уже начал стабилизироваться, и снова появляется время позависать на всяких сайтах с алгоритмическими задачками (успел даже поучавствовать в двух SRM’ах на TopCoder’е).
Например, сайт Красноярского Дворца пионеров и школьников. Он позиционируется как место для самоподготовки школьников, интересующихся программированием, для олимпиад. Но хвала тем школьникам, которые могут сходу решать задачи сложности 50% и выше с этого сайта. Не всякий, как пишут в резюме, “профессиональный программист с многолетним стажем” сможет “взять” некоторые из них.
Обнаружил просто мега-сайт - e-maxx.ru/algo. Справочник по алгоритмам и их реализациями на С++. Все на русском языке. Как мне тут сказали, что, в принципе, освоив большинство из них, можно показывать неплохие результаты на программистских соревнованиях.
В разделе книг там также отличная подборка.
Из категории, опять-таки, олимпиадного программирования, могу посоветовать:
С. Окулов, Программирование в алгоритмах
М. Долинский, Решение сложных и олимпиадных задач по программированию
Мне эти две понравились тем, что тут даются не просто сухие академические описания алгоритмов, как, например, у Седжвика, а примеры их применения и адаптации для реальных задач.
Кстати, заметил интересный факт. Лично я не люблю язык Паскаль. Мое субъективное мнение. Многословный и эстетически некрасивый синтаксис. Но Российская школа программирования уважает Паскаль и Дельфи (спасибо вечному Турбо Паскалю 7.0), и поэтому программы в этих книгах написаны на Паскале, а не на С++. И заставляя себя вчитываться в паскалевские куски кода, еще и изуродованные форматированием для печати в книге, получаешь более полное понимание алгоритма. Как говорят: умение читать код может быть даже важнее для программиста, чем писать его.
Далее. Нашел интересную страничну какого-то профессора из MIT, на которой лежат его мини видео лекции с разборами некоторых классических задач динамического программирования. Манера подачи материала очень интересна - видео представляет собой постепенную запись объяснения задачи от руки, сопровожаемую голосом. Например, задача о сдаче, или оба вида задачи о ранце - с повторами и без (0-1).
Алгоритмы - это очень интересно.
Посты по теме:
]]>Как это реально выглядит: просто у меня есть папка, которая автоматически синхронизируется через интернет на любом компьютере, где я залогинен в Dropbox. Не надо ничего вручную копировать и синхронизировать. Ты просто работаешь с локальными файлами, а все в фоне уходит на сервер и оттуда раздается на другие компьютеры.
Все файлы имеют историю ревизий, и их можно делать выборочно публичными. Почти как Google Docs, только есть автоматическое синхронизирование с локальными файлами.
Недавно вышел официальный клиент Dropbox для Android, так что теперь файлы еще и доступны прямо с телефона. Захотел в метро исходничек глянуть или pdf-ку - пожалуйста.
Конечно, класть в эту папку более менее конфиденциальные данные было бы глупо, хотя Dropbox гарантирует их сохранность, но для рабочих документов и исходников - это самое оно. Они и так у меня почти все лежат на Google Code.
]]>В принципе, тривиальная задача. Элементарное разложение на множители.
Как я ее решал. Прочитав определение совершенных чисел (до этого я не знал про такие числа, и далее будет понятно, что это было моей главной проблемой) и поняв, что мне надо разложить число на множители, я написал что-то вроде:
bool is_perfect(long long n) { if (n == 1) return false; long long s = 1; long long q = sqrt((double)n); for (long long i = 2; i <= q; ++i) { if ((n % i) == 0) { s += i; if (n/i != i) s += n/i; } } return s == n; } ... bool found = false; while (m <= n) { if (is_perfect(m)) { os << m << endl; found = true; } m += 1; } if (!found) os << "Absent" << endl; ...
Ничего оригинального. Работает на данных тестовых контрольных примерах. Прогоняю в системе. На одном из тестов мне сообщают, что программа работает более двух секунд, и это превышение данного временного лимита.
И только тут я смотрю на органичения задачи. А именно, что верхняя граница для N - это 5*10^18
, то есть если при этом дать M=1, то мой цикл должен будет пробежать ~10^18
значений, что в отведенные на это 2 секунды явно не укладывается.
Почесав репу, я начал гуглить, так как идей по ускорению алгоритма не было.
Первый же поиск раскрыл мне суть проблемы - а сколько вообще есть таких совершенных чисел? Оказывается, что на интервале от 0 до 5*10^18
их всего-то восемь, и они уже давно вычислены!
6, 28, 496, 8128, 33550336, 8589869056, 137438691328, 2305843008139952128
Поэтому вместо самостоятельного вычисления этих чисел надо просто найти, какие их этих восьми попадают в данный интервал [M, N]
. Как говорится “Easy peasy lemon squeezy!”.
Естественно, после этого решение успешно засабмитилось.
Мораль (кстати, верная не только для спортивного программирования) - начинать решение алгоритмической задачи следует с выяснения верхних ограничений входных данных, ибо чаще всего они подсказывают путь решения. Как это ни странно, почему-то вместо этого сразу хочется кодить, откладывая на потом осознание факта, что в ограничения-то программа не укладывается.
Мораль 2. Есть случаи, когда надо просто знать, как решать тот или иной тип задач. А знать это можно только хотя был раз их прорешав. Можно, конечно, и самому изобрести новый QuickSort с нуля, но это будет уже другая история.
P.S. Сейчас идет Google Gode Jam 2010.
В квалификации я решил полностью две задачи из трех, что достаточно для этого раунда. В раунде же 1 (я пробовал все его подтуры) я решал только одну задачу, что, увы, маловато.
Еще интересный момент. В подраунде Round 1B участвовал известный своим юным возрастом “спортивный” программист Геннадий Короткевич (третья позиция сверху таблице результатов). Я всегда очень люблю смотреть решения других людей. Поглядев на его решения, а был реально поражен тем, что они написаны на Дельфи! (а фактически на Паскале) (на Code Jam’е по статистике есть решения и на более “экзотических” языках типа BrainFuck’а или PostScript’а, но такие программы скорее всего сгенерированы из “традиционного” языка, и это уже иная тема). И эти решения кратки и понятны, и не используют различные шаблонные заготовки, которые в целом в ходу в спортивном программировании. Это еще одно подтверждение, что даже для алгоритмических задач “неудобный” язык не является проблемой в написании быстрой и понятной программы.
P.P.S. Один мой друг учавствовал в раунде 1, сидя с ноутом на полу в аэропорту через Wifi. Это не помешало ему получить 127-е место и проход в следующий раунд.
]]>Каким-то чудом и к великому сожалению сейчас вся эта тема олимпиадного программирования сильно меня не зацепила в школе и институте. Хотя там для этого было бы больше времени.
И самое что в этом всем отрезвляющие, что порой ступор наступает на казалось бы «очевидных» и типа «уже когда-то давно изученных» тобой задачах. Например, практически над каждой из серии «задач для обучения и закрепления материала» по динамическому программированию я изрядно чесал репу, хотя выглядят конкретно они весьма академично. И практически каждую я засабмитил далеко не с первого раза. Думается, что на том же ТопКодере каждая из них была бы первой задачей (то есть самой простой).
Вообще появляется некоторая зависимость. Зависимость от количества решенных задач. После нескольких успешных сабмитов хочется еще, еще и еще, а задачи становятся сложнее и сложнее. Своеобразное программистское судоку.
В принципе, тут необязательно маньячить на онлайн матчах, когда счет идет еще на время. Тут ты просто решаешь в свое удовольствие. Хотя, может желание соревновательности приходит с появлением опыта.
И все это – абсолютное знание. Твой единственный судья – это программа проверки результата и тестовые наборы данных, которые одинаковы для всех. Говорят, что на том же ТопКодере есть пути нечестного поднятия рейтинга, но очень несильно. Если посмотреть на топ-листы независимых онлайновых контестов, то можно увидеть там одни и те же имена. Неужели все эти люди знают как «читить» на всех сайтах? Конечно нет.
Притягивает тут абсолютность знания. Все что нужно, чтобы выиграть – это просто решить задачи быстрее всех. Казалось бы, что может быть проще. Как сказал мне тут один товарищ, что нейроны необходимо постоянно задр***вать, а иначе они теряют эластичность.
Сайтов для онлайнового решения задач много. Я пока остановился на Школа программиста, Timus Online Judge, UVa Online Judge (классика жанра), Codeforces и, конечно, TopCoder (пока только в practice room’ах).
В принципе, нет необходимости лазить по всем сайтам, так как каждый имеет изрядную тысячу и более задач, так что хватит решать не перерешать.
Единственное что важно, особенно сначала, когда можно удобно выбирать задачи по тематике и сложности. Вот тут и стоит покопаться на нескольких системах, ибо каталоги могут быть устроены по-разному.
Погрузившись в тему рекрутинга для нашей группы последнее время я только подкрепился в своей старой мысли, что резюме – это крайне бесполезный документ. Он красиво скажет о полностью неадекватном человеке, и порой ничего не скажет о действительно хорошем кандидате.
Если вместо сотни красивых слов в резюме про где и как человек учился и над чем замечательным работал, будет просто указана ссылка на профайл того же ТопКодера, то это будет в сотни раз полезнее. Там тебе и примеры кода, и тематика решенных задач, время из решения (после этого можно не спрашивать, как «человек работает под стрессом»), и, что очень важно с моей точки зрения – страсть к программированию, увлеченность (а как иначе можно объяснить многолетнее постоянное участие в «пустой трате времени», как спортивное программирование).
Конечно, есть огромное количество иных вопросов, которые следует прояснить на собеседовании, но здоровенный пласт касаемо алгоритмов и банального умения просто писать код, который работает, оказывается уже отработанным.
Например, другой товарищ рассказывал, когда его стандартно попросили у нас на интервью нарисовать функцию для вычисления чисел Фибоначчи разными способами – рекурсивно, рекурсивно с хвостовой рекурсией, а затем просто циклом, он написал матричный вариант через производящую функцию, который работает за логарифмическое время, вместо линейного. У товарища, кстати, неплохой рейтинг на ТопКодере.
Думается мне, что в принципе люди с относительно высоким рейтингом на ТопКодере проблем с трудоустройством не испытывают.
В общем, решайте задачи, тренируйтесь, держите себя в форме.
]]>Но все было гораздо проще. Как вы думаете, что должна выводить следующая программа?
#include <iostream> #include <cmath> using namespace std; int main(int argc, char* argv[]) { double f = 1.15; int a = f * 100.0 + 0.1E-9; int b = f * 100.0; cout << "a = " << a << endl; cout << "b = " << b << endl; return 0; }
Я ожидал два числа 115.
Нет, у меня на VS2008 она печатает:
a = 115
b = 114
Вот такие дела.
Update:
Кстати, если попробовать так:
#include <iostream> #include <cmath> using namespace std; int main(int argc, char* argv[]) { double f = 1.15; int a = f * 100.0 + 0.1E-9; int b = f * 100.0; cout << "a = " << a << endl; cout << "b = " << b << endl; double f1 = 0.15; int a1 = f1 * 100.0 + 0.1E-9; int b1 = f1 * 100.0; cout << "a1 = " << a1 << endl; cout << "b1 = " << b1 << endl; return 0; }
то результат будет:
a = 115
b = 114
a1 = 15
b1 = 15
Как я думаю, это из-за того, что числа, у которых целая часть нулевая имеют немного особое внутреннее представление в IEEE.
На ТопКодере есть отличная статья на эту тему (часть 1 и часть 2). Все кратко и по делу.
]]>Наибольшая общая мера последние 2500 лет
Посты по теме:
]]>«GUT» – это сокращение от Good Unit Tests.
Также я узнал выражения «Programming with BUTs» (Brittle Unit Tests) и «Programming with NUTs» (No Unit Tests).
Итак, «…with GUTS» - это хорошо, «…with BUTs» - хуже, «…with NUTs» - безумие.
Вся эта терминология построена на игре слов guts, butt и nuts в английском языке, которые своим двойным смыслом примерно отражают ситуацию в проекте, когда тестирование находится на указанном уровне (Good, Brittle, No).
Дядька очень интересно ведет беседу, нескучно. Вовремя переходит от приколов к важным выводам, что, например, что ошибки – это не зло, а данность (процесс создания программа сложен, и ошибки – это часть процесса), поэтому очень важно уметь использоваться их для своих целей. Глупо исправлять баг, не написав для него unit- или regression- тест, так как без теста информация, которую несет в себе баг-репорт, будет потеряна по исправления ошибки.
Не могу сказать, что я узнал что-то радикально новое для себя, хотя услышал несколько интересных мыслей и аналогий.
Например, дежурный вопрос менеджера (не очень умного менеджера) проекта: «А если мы будем практиковать TDD в разработке, это замедлит процесс?». И ваш правильный и политический верный ответ будет решительное «ДА!». «А правда, что написание тестов вместе с кодом призвано затормозить работу разработчика?». «ДА!!!».
В короткой перспективе – да, но в долгосрочной – нет, а совсем наоборот.
Есть хорошая аналогия. Можно ли доехать из места А в место Б на машине, у которой нет традиционных тормозов (а например, только ручник)? Теоретически да. Очень очень медленно, правильно используя торможение двигателем и ручником. Задача будет решена, но очень не быстро, и вероятность облажаться очень высока. А вот если машину оборудовать нормальными тормозами (~unit-тестами), цель которых останавливать машину (и увеличивать время достижения цели), то они также позволят активно разгоняться между торможениями, и как результат – приехать быстрее.
Мораль – тесты позволяют взять ваш код под контроль, превратить его из телеги, несущейся с горы на огромной скорости, в гоночный болид, который ездит также быстро, как и останавливается. Тесты радикально затормозят вас, когда это нужно. Но с другой стороны они позволят не дрожать всем телом, когда надо потрогать какой-то очень важный кусок кода (например, библиотеку строчек), а спокойно провести рефакторинг и проверить результаты.
Все эти посылы просто TDD очевидны и однозначно разумны, но я был поражен до глубины души, когда кто-то таки спросил из зала, а если я, мол, просто буду писать много комментариев, чтобы всем было понятно, что код делает. И это был не вопрос-прикол. Кто-то спросил это на полном серьезе.
По поводу комментариев была приведена также интересная аналогия. Комментарии в коде – это золото. Но золото обесценивается, когда его становится слишком много. Комментариев много бывает (особенно бесполезных).
На лабах было интересное задание. Программисту дается задание написать класс и unit-тесты для него. Затем эти тесты уже без кода класса даются другому программисту, и его просят воспроизвести функционал класса, основываясь только на тестах к нему. Результаты порой «радуют» даже продвинутых бойцов TDD.
Вывод как всегда простой: тестируйте как можно раньше, тестируйте чаще, тестируйте автоматизировано. Тестирование – это часть работы программиста в первую очередь, а тестера – во вторую.
P.S. Небольшое задание для интересующихся. Допустим, вы написали собственный алгоритм сортировки. Как его тестировать? Каковы будут критерии проверки, что результат такой, какой вы ожидаете?
]]>n &= (n - 1);
просто потому, что это весьма распространенный шаблон, и для чего может понадобиться выполнение его в цикле, пока n
не станет нулем.
Интересно другое: четкое математическое (и/или алгоритмическое) объяснение, почему это работает именно так, строгое доказательство.
Update
Чтобы разобраться в вопросе надо понять, что является корнем недопонимания.
Для людей, не часто имеющих дело с двоичной системой, обычно не совсем очевидна суть трансформации внутреннего двоичного представления числа при выполнении арифметической операции в десятичной нотации. Кажется, что если вычесть единицу, то расположение битов после операции будет иметь мало общего с тем, что было до вычитания. И дальнейшее выполнение and
вообще не имеет смысла.
С точки зрения битового представления любого числа, есть только два случая:
Если число нечетное, на конце будет единица: xx...xx1
. Вычитание из такого числа даст: хх...хх0
. Поэтому (xx...xx1) & (xx...xx0)
даст (xx...xx0)
. Фактически, мы убрали младший бит.
Если число четное, на конце будет ноль (или несколько нулей): xx...xx100...00
. Видно, что вычитание единицы из такого числа однозначно не изменит разряды xx...xx
, стоящие слева после первой единицы. Более того, результат вычитания единицы однозначно предсказуем: xx...xx011...11
. Теперь точно видно, что будет после операции and
: (xx...xx100...00) & ("xx...xx011...11")
даст xx...xx000...00
. То есть мы убрали единицу из самого младшего ненулевого разряда.
Теперь ясно видно, что именно проиходит в присваивании n &= (n - 1);
. А именно, обнуление самого младшего ненулевого разряда.
Использование этого трюка в цикле, пока n не равно нулю, позволяет подсчитать количество ненулевых бит. На каждой итерации мы “выбиваем” ровно один бит, поэтому число итераций будет равно количеству единиц в n
.
На меня это произвело весьма сильное впечатление, особенно, когда многое собрано в одном месте и работает прямо в самой “презентации”.
У меня под Chrome все работает. Не уверен про IE, но, говорят, что под FireFox тоже все работает.
]]>Пока обзор будет краткий (фактически, это просто перевод официального анонса):
assert
‘ы теперь можно безопасно запускать из разных потоков (работает на платформах, где есть pthreads)EXPECT_TRUE()
теперь можно самому задавать сообщения их ошибкахgtest.h
и gtest-all.cc
, которые можно добавить в проект и не возиться с двоичной библиотекой)CMake
(это фантастически удобно)Для тех, кто слышит про Google Test впервые, ниже предыдущие посты об этой библиотеке и о тестировании в целом (многие на русском языке):
Найти середину связного списка за один проход. Длина заранее не известна, а есть только указатель на голову.
Кто не догадается, смотрите комментарии. Уверен, там задачу быстро “возьмут”.
Задача очень жизненная, и реально полезно знать решение.
Как научиться решать задачи подобного рода? алгоритмические программистские задачи? В чем суть обучения?
Есть определенная категория программистов, которым я профессионально завидую. Это бойцы спортивного программирования типа TopCoder’а, Google Code Jam’а и прочих контестов. Для которых что-то вроде поиска цепочек в частично упорядоченном множестве как для меня сложить две переменные. Там, где мне надо открыть книгу или хотя бы Википедию, они это пишут сходу из головы.
Из личного опыта, перед некоторыми интервью, я мог на память сходу написать любую базовую сортировку, вставку в красно-черное дерево, послойный обход дерева и многое другое. Но проблема в том, что это ни разу не помогало на интервью! Все что я в итоге решал (или не решал), было связанно с задачами, которые могли быть в разы проще любой сортировки или дерева, но требовали “догадаться”. И основном спортивное программирование построено на теме “догадаться как”.
Но как научиться догадываться? Есть ли какая-то методика обучения? Именно методика, а не “решай все подряд, и все тебе будет”. Второй подход, конечно, работает, но методика могла бы ускорить процесс, сделать его более эффективным.
Например, взять физику или математику. Обычно ты учишь теорию и ее отражение на физический мир вокруг нас, учишь уже выведенные законы, теоремы (возможно их доказательства) и построенные на них базовые формулы. Далее при решении конкретной задачи ты, основываясь на выученных основах, пытаешься понять, что за физическая или математическая модель могла бы иметь место в задаче. Далее пытаешься приложить эту модель и смотришь, что получается (или не получается).
То есть тут есть методика, которая помогает тебе найти связь между теорией и задачей. Конечно, есть задачи, в которых “надо догадаться”, но это как опция, нежели норма. Тебе дается инструмент, когда зная малое, можно решать многое.
В нашем же программистском мире, как мне кажется, все из серии “надо догадаться”. Взять, например, эту задачу про поиск середины связного списка в один проход. Я могу изучить всё про такие списки: как их строить, как их сортировать, как их обходить (просто линейно) и т.д. Но все эти знания не подвинут меня ни шаг к решению этой простой задачи. Тут надо “просто догадаться”. Как научить догадываться? Неужели есть только один путь - решать и решать, пока количество не перейдет в качество, и ты будешь легко решать задачи на “догадаться” просто потому, что ты это уже так или иначе видел раньше.
Кстати, сравним среднего выпускника MIT и МАИ (я там учился). Наверное, выпускник MIT потенциально “сильнее” выпускника МАИ (при всей моей любви и уважении к Альма-матер). Почему? Там книги другие? Там профессора хуже? Я не могу пожаловаться, что мой курс программирования, где изучались алгоритмы был недостаточен.
Пока у меня только одно объяснение - скажем так, в объективно престижных ВУЗах просто больше прессуют и фактически заставляют учиться (иначе - это пустая трата денег и большой шанс вылететь).
Выходит, что количество так или иначе переходит в качество. И никакой магии.
]]>float
или double
. Также финансовой сфере много старого когда, написанного на С или Фортране.
А в мире языке С практика инициализации структур нулем через memset является весьма распространенной и в целом не самой плохой практикой.
Вопрос: а что, если в структуре есть поля типа double
или float
. Что будет, если поля этих типов будут тупо забиты нулями, каково будет значение этих полей?
Для начала я проверил у себя на Солярисе и в Visual Studio 9 - все вроде нормально. После memset
‘а нулем и float
и double
тоже равны нулю.
Хотя в целом правильный ответ такой: если ваш компилятор гарантирует хранение вещественных чисел в форматe IEEE 754, то вы в безопасности. Если нет (стандарт языка не гарантирует, что должен использоваться именно IEEE 754), то могут быть неожиданности.
]]>Например, задача, часто применяемая (не мной) на интервью.
Из двух городов навстречу друг другу едет велосипедист и летит муха. Скорость мухи в четыре раза быстрее велосипедиста. Когда велосипедист и муха встречаются где-то по середине, муха разворачивается и летит назад, а велосипедист также едет себе дальше. Когда муха возвращается в исходную точку, она разворачивается и снова летит на встречу велосипедисту. Когда они встречаются, все повторяется снова. Муха курсирует подобным образом от своего города до велосипедиста и назад то тех пор, пока велосипедист не приедет в ее город. Вопрос: каково соотношение расстояний, пройденных мухой и велосипедистом?
Задача для средней школы. Хотя лично мне будет без разницы, решит ее человек или нет. Ибо есть гораздо более конкретные вопросы и задачи, чтобы понять, что человек знает, умеет и любит делать. Обычно использование подобных задач призвано потешить самолюбие самого интервьюера типа «ну я то это решил в свое время!», но вот только не уточняется, за какое время.
В плане именно вопросов-задач, куда полезнее попросить написать небольшой код для обхода дерева (например, для поиска медианы в BST), или поиска кратчайшего пути в графе, или придумать на месте хеш-функцию для каких-то данных, или, например, спросить: до N какого порядка реально решать задачи с O(N^2), а O(N^3), а на кластере? и т.д. Подобные вопросы дают реальный выхлоп, который применим в жизни.
А уж если есть желание «проверить сообразительность», то куда полезнее вопросы на общее представления о мире вокруг и взаимосвязях в нем. Я как-то был реально озадачен, когда меня спросили «А сколько весит Боинг-747?». Я хоть и учился в МАИ, но точной цифры ни разу не знал. Но исходя и элементарных понятий и количестве людей в самолете, грузе, соотношении топлива и массы и т.д. – можно дать оценку, или что более важно, показать умение логически и реально мыслить.
Интервью – это не бенефис интервьюера. Это решение конкретной задачи поиска человека, максимально подходящего вашим требованиям (а неужели у кого-то в требованиях написано «должен уметь решать задачи школьной программы»? если да, это наем учителя средних классов, а не программиста).
]]>Bernard Girard, “The Google Way”
Не могу сказать, что мне особо понравилось, хоть фактов в книге предостачно: и про самое начало, и про выход на IPO (работая сейчас в сфере finance, знаю, как обычно инвестиционные банкиры наживаются на процедуре IPO, но Гугл их сильно в этом плане разочаровал, не дав им кусок своего пирога), и про правило 20%, и про систему peer review, и про наличие технической иерархии и про многое другое.
Разок прочитать можно, но не более того. Книгу продал назад через Amazon Market Place.
]]>Поймал людоед несколько гномов (количество в задаче не задается), и сказал, что завтра он всех выстроит в колонну один за другим и оденет всем на головы либо черную, либо белую шапку. Гномы будут стоять так, что каждый будет видеть шапки только тех, кто впереди (последний видит всех, кроме себя, а первый – никого). Свою собственную шапку гномы не видят. Количество черных и белых шапок произвольное (хоть все белые, хоть все черные). Далее людоед, начиная с последнего, будет каждого спрашивать, какая шапка у него на голове. Гном может ответить только одним из двух слов «черная» или «белая». Если ответ неверный, но гном съедается, иначе переходят к следующему. В процессе поедания все пока еще живые гномы слышат, что проиходит сзади, то есть хоть они и не видят товарищей сзади, но слышат – когда кого съели, а кого нет, и также их ответы.
В общем, за ночь гномы покумекали, и придумали стратегию, как они все останутся живыми кроме одного. Но и тот один будет иметь шанс выжить.
Вопрос: что за стратегию придумали гномы?
Задача имеет очень красивое решение, имеющее прямое отношение к компьютерной науке.
Ответ будет позже, если кто вдруг не догадается.
]]>Jason Fried, David Heinemeier Hansson, “Rework”
Довольно пафосная книга, состоящая из мини глав, похожих на посты в блоге (эта книга и родилась из блога), но многие ее посылы стоит усвоить. С ними легче и приятнее жить, и во многом ранее скучном и нудном может появиться вдруг смысл. Также книга может подтолкнуть в мир самостоятельной работы на самого себя, например, написание shareware.
Но одна глава мне очень понравилась. Про резюме и сопроводительные письма.
Приведу ее целиком как есть и собственный вольный перевод, если кому надо.
Resumes are ridiculous
We all know resumes are a joke. They’re exaggerations. They’re filled with “action verbs” that don’t mean anything. They list job titles and responsibilities that are vaguely accurate at best. And there’s no way to verify most of what’s on there. The whole thing is a face.
Worst of all, they’re too easy. Anyone can create a decent-enough resume. That’s why half-assed applicants love them so much. They can shotgun out hundreds at a time to potential employers. It’s another form of spam. They don’t care about landing your job; they just care about landing any job.
If someone sends out a resume to three hundred companies, that’s a huge red flag right there. There’s no way that applicant has researched you. There’s no way he knows what’s different about your company.
If you hire based on this garbage, you’re missing the point of what hiring is about. You want a specific candidate who cares specifically about your company, your products, your customers, and your job.
So how do you find these candidates? First step: Check the cover letter. In a cover letter, you get actual communication instead of a list of skills, verbs, and years of irrelevance. There’s no way an applicant can churn out hundreds of personalized letters. That’s why the cover letter is a much better test than a resume. You hear someone’s actual voice and are able to recognize if it’s in tune with you and your company.
Trust you gut reaction. If the first paragraph sucks, the second has to work that much harder. If there’s no hook in the first three, it’s unlikely there’s a match there. On the other hand, if your gut is telling you there’s a chance at a real match, then move on to the interview stage.
Перевод:
Всем известно, что резюме – это ерунда. Одни преувеличения. В них полно «правильных» глаголов, которые ничего не значат. В них перечисляются должности и обязанности, которые в лучшем случае сомнительны. И нет никакой возможности проверить, что за всем этим стоит. Сплошной фарс.
И хуже все то, что их очень просто сделать. Любой может написать приличное резюме. Вот почему недоразвитые кандидаты так их любят. Они могут наплодить их сотни для потенциальных работодателей. Это еще один тип спама. Им не важно получить работу у вас. Им нужна какая-то работа.
Если кто-то посылает резюме в три сотни компаний, это плохой знак. Такой кандидат не пытался ничего о вас узнать, и чем ваша компания отличается от других.
Если вы нанимаете, основываясь на такой ерунде, теряется сам смысл найма. Вам нужен конкретный человек, интересующийся именно вашей компанию, вашими продуктами, вашими клиентами и вашей работой.
Так что делать, чтобы найти таких кандидатов? Во-первых, обратите внимание на сопроводительное письмо. В нем вы найдете настоящее обращение от кандидата, а не список умений, глаголов и кучи всего, не относящегося к делу. Вряд ли кандидат будет писать сотни личных писем. Поэтому сопроводительное письмо – это гораздо лучшая проверка, чем резюме. Вы услышите голос живого человека и сможете понять, подходит ли он вам и вашей компании.
Доверяйте нутру. Если в первом параграфе все плохо, во втором шансов уже мало. Если первые три вас не зацепили, вряд ли это ваш случай. С другой стороны, если вы чувствуете, что-то тут есть – переходите к интервью.
От себя могу добавить. Всегда стоит писать cover letter, причем вдумчивые и персонализированные. В процессе их написания часто приходит понимание - “а хочу ли сам я там работать?”
Книгу после прочтения я продал назад через Амазон.
]]>Нас просят привести одну последовательность (исходную) к другой (целевой). То есть логично предположить, что одна последовательность в нужном порядке (целевая), а вторая (исходная) - нет. Так надо просто отсортировать исходную последовательность “в целевую”.
Так как целевая последовательность по условию не обязательно отсортированная, то при сортировке “к ней” нельзя просто сравнивать элементы исходной последовательности на больше/меньше, так как в этом случае мы получим на выходе сортировку по правилам системы исчисления. В нашем случае надо принять, что целевая последовательность и есть эталонный отсортированный алфавит, и он задает правила сортировки. При сравнении значений из этого алфавита надо понять, в какой позиции алфавита находится значение и использовать его индекс как ключ сортировки (функция less()
).
Теперь, а какой алгоритм сортировки использовать, чтобы для перемещения элементов использовать только обмен соседних элементов (функция swap()
)? Подходит сортировка вставками, когда на каждом шаге неотсортированный элемент последовательно “пропихивается” вниз к отсортированным. Тут как раз можно обойтись только обменом соседних элементов. Сама функция insertion_sort()
является универальной и не зависит от компаратора is_less()
.
#include <stdlib.h> #include <stdio.h> void swap(int* a, int i) { int t = a[i]; a[i] = a[i + 1]; a[i + 1] = t; } #define N 8 const int etalon[] = { 1, 5, 7, 4, 2, 9, 8, 6 }; int from[N] = { 8, 1, 4, 2, 5, 6, 9, 7 }; void insertion_sort(int* a, int n, int (*is_less)(int, int)) { int i, j; for (i = 1; i < n; ++i) for (j = i - 1; j >= 0 && is_less(a[j + 1], a[j]); j--) swap(a, j); } void print_array(const char* title, const int* a, int n) { int i; printf("%9s: ", title); for (i = 0; i < n; ++i) printf("%d ", a[i]); printf("\n"); } int less(int a, int b) { int ia = -1, ib = -1; int i; for (i = 0; i < N; ++i) { if (etalon[i] == a) ia = i; if (etalon[i] == b) ib = i; if (ia >= 0 && ib >= 0) break; } return ia < ib; } int main() { int i, j; print_array("Original", from, N); insertion_sort(from, N, less); print_array("Converted", from, N); print_array("Etalon", etalon, N); return 0; }
Запускаем:
Original: 8 1 4 2 5 6 9 7
Converted: 1 5 7 4 2 9 8 6
Etalon: 1 5 7 4 2 9 8 6
Вроде работает.
Теперь что со сложностью. Принято считать, что сортировка вставками - это O(N^2)
для худшего случая. Так как для сравнения элементов нам приходится искать линейно по эталонной последовательности на каждом шаге, то это еще O(N)
. В этоге: O(N^3)
.
Как вариант ускорения, можно изначально сделать отсортированную по значениям копию эталонной последовательности, и хранить не только значение, но его индекс. В этом случае поиск элемента будет уже занимать не O(N)
, а O(log(N))
, и общая сложность будет O(log(N)*N^2)
.
В целом, все это не обязательно писать или помнить точную программу. Достаточно запомнить два вывода: алгоритм сортировки, использующий только обмен соседних элементов - это сортировка вставками, а ключ сортировки может быть далеко нетривиальной функцией.
Указание на эти два факта лично я счел бы на однозначно достаточный ответ.
]]>parseInt()
‘e.
Я думал, что код ниже должен давать мне числа от 0 до 9:
var n = [ '00', '01', '02', '03', '04', '05', '06', '07', '08', '09' ]; for (var i = 0; i < n.length; ++i) document.writeln(parseInt(n[i]))
Но выводится:
0 1 2 3 4 5 6 7 0 0
И это поведение законно, так как лидирующие нули рассматриваются как признак восьмеричного числа, а 8 и 9 не являются восьмеричными знаками.
Правильно надо писать так:
var n = [ '00', '01', '02', '03', '04', '05', '06', '07', '08', '09' ]; for (var i = 0; i < n.length; ++i) document.writeln(parseInt(n[i], 10));
По хорошему, второй аргумент parseInt()
‘а, задающий систему исчисления, должен быть обязательным, чтобы исключить путаницу.
home
каталога всегда одно и то же на всех машинах (не надо вручную копировать файлы с машины на машину).
Минуту назад я не смог залогиниться xterm’ом из-за каких-то временных проблем в сети и попросил коллегу проверить, все ли работает. У него получилось, и он типа сказал в шутку, что жаль не может передать мне его телнетное окно.
Так вот, только родилась мысль – а что, если все разработчики будут также “шарить” общий огро-о-о-мный виртуальный десктоп? Захотел что-то передать/показать коллеге – просто перебросил окно в его “зону видимости”.
Идея, конечно, утопичная, но красивая.
]]>Но как мне кажется, алгоритмы – это не единственная сфера интересов нашего брата. Есть еще языковеды, которые будучи разбуженные ночью могут сходу правильно написать любой хитрый шаблон в С++ или связку задорных лямбда функций, вместо унылых циклов и условий в Питоне и т.д., так как досконально знают язык во всех мелочах.
Далее есть спецы по различным API – например, им не надо глядеть в man каждый раз при вызове socket (7)
, чтобы вспомнить, как там правильно подавить появление сигнала SIGPIPE
и т.д.
Дальше идут знатоки проектирования. Всякие темы типа ООП, например, когда именно лучше применять агрегирование, а когда композицию и т.д.
За ними шагают апологеты тестирования – они во всех изматывающих подробностях знают, как правильно, например, написать конструктор класса, что класс было просто тестировать.
Может, есть и еще категории (если кто знает, прошу делиться).
К чему я все это веду. Грамотно, когда при собеседовании кандидатов (лично или по телефону) задействуют обычных программистов, причем желательно выбранных случайно, и желательно, чтобы проводящий интервью имел минимум личной информации о кандидате, так как его задача оценить только технический уровень, а всякие темы типа мотиваций и причин оставить на отдел кадров. Идеалистичная картина, но к ней хочется стремиться.
У нас контора весьма крупная. Количество программистов более нескольких сотен. Есть из кого выбирать для интервьюирования. Многие не любят учувствовать в этом, но лично я люблю. По нескольким причинам. Во-первых, постоянно есть возможность глядеть на себя со стороны, так как если человек по телефону как орехи разделал все твои вопросы или поставил тебя в тупик встречным вопросом, то может ты как-то уже профессионально «засахарился»? Или если большинство коллег дали совершенно противоположную твоей оценку, может ты спрашиваешь какую-то ерунду и думаешь, что это важно? А во-вторых, есть появляется возможность быть в курсе, что вообще сейчас на рынке труда.
Теперь совсем к теме. Интересно слушать, как люди проводят интервью по телефону. Что они спрашивают, на чем делают акценты. Тут сразу становится видно, кто какой категории принадлежит человек (алгоритмист, языковед, API’ист, теоретик и т.д.)
Конечно, в продуктивном программисте весь должно быть развито. Какой толк от алгоритмиста, который напишет так, что другой никогда не поймет и тем более не протестирует? Или от теоретика, не имеющего представления о вычислительных сложностях базовых алгоритмов?
]]>std::vector
. Но вот и по нему попался интересный вопрос.
Представим, что стратегия управления внутренним буфером контейнера std::vector
(в реальности, она иная) такова: изначально размер буфера равен нулю, и он будет увеличивается вдвое каждый раз, когда в нем уже нет места под следующий элемент.
Вопрос: оценить вычислительную сложность последвательного добавления в контейнер k элементов (как уже говорилось, начальная длина контейнера нулевая). Элементы добавляются в конец.
Как я полагаю, в среднем ожидается, что отвечать стоит практически сразу.
На всякий случай: мой ответ будет завтра.
А сейчас мини эксперимент с реальным std::vector
(компилятор, и сообразно STL — Sun C++ 5.9 SunOS_sparc) для выяснения реальной стратегии роста буфера в векторе:
#include <vector> #include <iostream> #include <iomanip> #include <cstdlib> int main() { int last = -1; std::vector<int> a; std::cout << std::setw(12) << "Capacity" << " " << std::setw(12) << "Size" << std::setw(12) << "Ratio" << std::endl << std::setw(12) << std::setfill('-') << "-" << " " << std::setw(12) << "-" << " " << std::setw(12) << "-" << std::endl; std::cout << std::setfill(' ') << std::fixed; while (1) { if (a.capacity() != last) { last = a.capacity(); std::cout << std::setw(12) << a.capacity() << " " << std::setw(12) << a.size() << " " << std::setw(12) << std::setprecision(6) << (float)a.capacity() / a.size() << std::endl; } a.push_back(std::rand()); } return 0; }
А вот и результат:
Capacity Size Ratio
------------ ------------ ------------
0 0 NaN
32 1 32.000000
64 33 1.939394
103 65 1.584615
166 104 1.596154
268 167 1.604790
433 269 1.609665
700 434 1.612903
1132 701 1.614836
1831 1133 1.616064
2962 1832 1.616812
4792 2963 1.617280
7753 4793 1.617567
12544 7754 1.617746
20296 12545 1.617856
32838 20297 1.617875
53131 32839 1.617924
85965 53132 1.617952
139091 85966 1.617977
225049 139092 1.617987
364129 225050 1.617992
589160 364130 1.617994
953260 589161 1.617996
1542374 953261 1.617998
2495561 1542375 1.617999
4037817 2495562 1.617999
6533187 4037818 1.617999
10570696 6533188 1.618000
17103386 10570697 1.618000
27673278 17103387 1.618000
44775363 27673279 1.618000
72446537 44775364 1.618000
117218496 72446538 1.618000
189659526 117218497 1.618000
306869113 189659527 1.618000
Выходит, что для моей STL - это какой-то магический коэффициент 1.618.
Update: В комментариях подсказали хорошую ссылку на тему стратегии управления размером вектора.
Update 2: Лично мой ответ на тему вычислительной сложности последовательного добавления элементов в вектор, если вектор будет удваивать размер буфера при переполнении.
Так как мы добавляем k
элементов, то это как минимум O(k)
. А так как по условию вектор удваивает буфер каждый раз, когда нет места, то произойдет log2(k)
раз (так как по условию элементы поступают последовательно).
Получаем в этоге: O(k*log2(k))
.
Update 3: В комментариях меня поправили: O(k + log2(k))
или просто O(k)
.
Вопрос: как на месте преобразовать первую последовательность во вторую, если в плане перемещения/копирования элементов есть только одна функция - обмен двух соседних элементов. Произвольный доступ к одиночным элементам есть только на чтение. Для перезаписи/изменения элементов надо пользоваться данной функцией обмена.
Конечно, интересно иметь и оценку сложности.
Мой ответ, если кому интересно, будет завтра.
]]>Называется “EcoDisc”.
Пластина более чем в два раза тоньше обычного DVD и от этого элементарно гнется. Сразу напомнило старые дешевые заменители нормальных виниловых пластинок голубого цвета, которые можно было сворачивать с трубочку. Я с таких в детстве слушал аудиосказки.
Но в целом читаемый, нормально сбалансированный DVD, от которого привод не стремится на взлет.
]]>В середине 90-x, в эхо-конференции Фидо RU.HACKER, небезысвестный Solar Designer (он же Alexander Peslyak, автор таких вещей как John The Ripper и Openwall Linux), опубликовал интересную программу собственного сочинения, и предлагал попробовать ее взломать, угадав пароль, который она запрашивает и проверяет.
В отличие от большинства подобных hackme, в программе не было трюков, затрудняющих работу отладчика. Единственная трудность - это логика самой программы, так весь ее исполняемый код - это менее 100 байт интерпретатора виртуальной машины, которая умеет делать только одну операцию - NOR (или Стрелку Пирса).
У этой виртуальной машины память линейна и стоит из 16-х битных слов. Данные и исполняемый код могут перемешиваться. Каждая инструкция состоит из трех слов - адресов операндов. Исполнение инструкции - это в чтении из памяти двух слов, адреса которых лежат в первых двух операндах, проведения между ними операции NOR и записи результата по адресу, заданному в третьем операнде. После выполнения инструкции указатель команд увеличивается на 3 (чтобы указывать на следующую инструкцию), и все повторяется сначала.
Трюк тут в том, что указатель на текущую инструкцию находится в адресном пространстве интерпретатора, поэтому для реализации команды перехода надо просто записать значение в ячейку, где этот указатель хранится. Сам интерпретатор очень прост и может быть написан на чем угодно. В оригинальной программе он был ассемблере x86:
cld emCPU: mov si,emIP lodsw xchg ax,di mov di,[di] lodsw xchg ax,bx or di,[bx] lodsw xchg ax,di not ax mov emIP,si stosw jmp short emCPU
Но ни что не мешает перенести его в мир, например, Питона:
def nor(a, b): return ~(a | b) & 0xFFFF def norcpu(): while 1: i = mem[IP]; a = mem[i + 0] b = mem[i + 1] r = mem[i + 2] mem[IP] = i + 3 f = nor(mem[a], mem[b]) mem[r] = f
Почему именно NOR? Из теории булевой алгебры известно, что любую из 14-ти логических функций (например, NOT, AND, OR, XOR и т.д., всего их 16) двух одно битовых аргументов можно выразить через функции NOR и NAND. Например:
NOT(a) = NOR(a, a)
AND(a, b) = NOT(OR(NOT(a), NOT(b)))
OR(a, b) = NOT(NOR(a, b))
XOR(a, b) = OR(AND(a, NOT(b)), AND(NOT(a), b)))
Пересылка данных MOVE(src, dst)
может быть сделана через OR:
mem[dst] = OR(mem[src], mem[src])
Условный переход также реализуется через булеву логику. Если cond равно 0xFFFF (истина), то осуществляется переход на адрес addr
, а если cond
равно 0x0000, то выполнение продолжается линейно:
mem[IP] = OR(AND(addr, cond), AND(mem[IP], cond))
или в нотации интерпретатора:
AND addr, cond, @t1
AND IP, cond, @t2
OR @t1, @t2, IP
где @t1
и @t2
- некоторые вспомогательные переменные. Команды AND и OR также раскрываются в последовательность элементарных NOR, как было показано ранее.
Что получается: булевы операции есть, пересылка данных есть, команды условного и безусловного переходов есть - для полноты не хватает операций сложения/вычитания и сдвигов. После этого уже можно программно реализовать стек и получить полноценную вычислительную среду.
Вот тут в оригинальной программе Александра был трюк, с помощью которого можно было вызывать native код x86. Так как код самого интерпретатора на ассемблере x86 также находился в адресном пространстве виртуальной машины, то в нужный момент в начало интерпретатора командой MOVE
подставлялась двухбайтовая команда перехода x86 (то есть интерпретатор сам себя модифицировал), по адресу перехода которой находился нужный native код x86. После его выполнения восстанавливались оригинальные первые два байты интерпретатора, интерпретация продолжалась в обычном режиме.
Именно так была реализована команда сложения и несколько утилитарных вызовов прерываний DOS для печати на экран и ввода с клавиатуры.
Лично я не представляю, как можно реализовать полноценное суммирование только через булевы функции. Полный сумматор может сложить два бита, но для чтобы учесть бит переноса при сложении следующего разряда его надо туда сдвинуть, а в текущей реализации интерпретатора сдвигов нет.
Кроме того, если писать интерпретатор на произвольном языке, например Питоне, то для выполнения стороннего кода, который не получается реализовать в рамкам интерпретатора, надо как-то перехватывать управление, и идея машины только на одной операции NOR перестает быть “чистой”.
Лирическое отступление. Я всегда интересовался reverse engineering’ом (даже сейчас не прочь покопаться IDA’ой в каком-нибудь экзешнике), и в частности различными идеями защиты от отладки. А данная идея интерпретатора одной команды тут подходит как нельзя лучше. Так как все реализовано через одну элементарную функцию, то при анализе и дизассемблировании сложно понять, где границы высокоуровневых команд. Как одну из идей, я переделал свой простейший Форт-интерпретатор с прямым шитым кодом (который сам по себе затруднен для анализа) под использование NOR интерпретатора для реализации всех низкоуровневых Форт-примитивов.
Недавно я вернулся к теме NOR-интерпретатора. Интересно было написать это на Питоне. Также пришла мысль, как можно модифицировать интерпретатор, чтобы на нем можно было бы реализоваться и полноценное сложение.
Я ввел одну дополнительную команду в него — циклический сдвиг результата каждой операции и сохранение значения в специальной ячейке-регистре:
def norcpu(): while 1: i = mem[IP]; a = mem[i + 0] b = mem[i + 1] r = mem[i + 2] mem[IP] = i + 3 f = nor(mem[a], mem[b]) mem[r] = f mem[S] = ((f >> 31) & 1) | ((f & 0x7FFF) << 1)
То есть теперь есть две особые ячейки: IP
(instruction pointer) и S
(значение последней операции, циклически сдвинутое влево).
Попробуем реализовать полное суммирование 16-х слов с переносом. Я буду писать на некотором элементарном макро-ассемблере.
Итак, полный однобитный сумматор в нотации булевой алгебры:
sum = (a ^ b) ^ carry
carry = (a & b) | (carry & (a ^ b))
Теперь на языке NOR-процессора:
; Вход:
; mask - битовая маска рабочего бита (0x0001, 0x0002, 0x0004, 0x0008 и т.д.)
; carry - перенос от предыдущего бита (бит определяется маской mask)
; a, b - адреса аргументов
; r - адрес результата
; Выход:
; r - результат
; carry - перенос для следующего разряда (по отношению к mask уже сдвинут на 1 бит влево)
; mask - маска, сдвинутая слево на 1 бит
;
; Переменные с префиксом '@' - локальные для этого макроса.
;
!macro FADD mask, carry, a, b, r
AND a, mask, @bit_a ; Уберем в "a" все биты, кроме нужного.
AND b, mask, @bit_b ; Уберем в "b" все биты, кроме нужного.
AND carry, mask, carry ; Уберем в переносе все биты, кроме нужного.
XOR a, b, @t1 ; Формула: sum = (a ^ b) ^ carry.
XOR @t1, carry, @bit_r ;
AND @bit_r, mask, @bit_r ; Уберем из @bit_r все биты, кроме текущего
; по маске.
OR @bit_r, r, r ; Наложим текущий сложенный бит на результат:
; r |= sum
AND a, b, @t2 ; Формула: carry = (a & b) | (carry & (a ^ b))
AND carry, @t1, @t1 ;
OR @t2, @t1, carry ; Перенос получил новое значение. Регистр S
; равен ему же, но сдвинутому влево на 1 бит.
MOVE S, carry ; Теперь перенос равен себе же, но со сдвигом
; влево на 1 (для суммирования в следующем бите).
MOVE mask, mask, mask ; Пустое присваивание mask самой себе, чтобы
; получить сдвинутое значение в S.
MOVE S, mask ; Маска сдвинута влево на 1 бит для суммирования
; следующего бита: mask = S = mask << 1
И теперь сам макрос полного суммирования:
; Вход:
; a, b - аргументы
; carry - перенос (0 или 1 в младшем разряде)
; Выход:
; r - результат
; carry - перенос (0 или 1 в младшем разряде)
;
; Переменные с префиксом '@' - локальные для этого макроса.
; const_1 - специальная ячейка, в которой содержится 0x0001.
;
!macro ADC a, b, carry, r
XOR r, r, r ; Запишем 0 в r.
MOVE const_1, @mask ; Начальное значение маски: 0x0001
*16 FADD @mask, carry, a, b, r ; Повторяем FADD 16 раз (просто линейно в
; памяти, никаких циклов).
AND carry, const_1, carry ; Почистим перенос от мусора в старших
; разрядах.
Что происходить в ADC
? При каждом повторении FADD
происходит суммирование текущего бита, маска которого задана в mask
. Просуммированный бит добавляется (через OR) в результат. Кроме этого mask автоматически сдвигается влево на 1 бит, чтобы указывать на следующей бит (0x0001 -> 0x0002 -> 0x0004 и т.д.). Также в каждом вызове FADD
перенос после суммирования тоже сдвигается влево на 1 бит, чтобы быть готовым для суммирования на следующей итерации. После суммирования последнего 16-го бита перенос уйдет снова в самый младший разряд (так как интерпретатор делает циклический сдвиг), и это значение и будет результирующим переносом после всего суммирования.
Все, сложение у нас есть. Далее дело техники. Реализация стека чисто программная. Команды вызовы подпрограммы и возврата на место вызова реализуются уже через механизм стека и команд перехода.
Итак, что мы имеем в сухом остатке после муторных битовых баталий? Некую битовую виртуальную машину, на которой можно делать любые вычисления.
Машина крайне проста, но из-за этого программный код, состоящий из примитивных NOR’ов может быть большим.
Для чего? Первое и главное: академический интерес. Прикольно же получить полноценную вычислительную среду на базе единственной операции NOR. Второе: изначально все это задумывалось как вариант защиты, например, от копирования. На данной виртуальной машине можно реализовать хитрую крипто-функцию и ей проверять валидность ключа. Таким образом к крипто-защите еще и добавится трудный анализа кода.
Но на дворе время open source, то что повторюсь - академический интерес!
Посты по теме:
]]>a += b; b = a - b; a -= b;
Интересно разве что с академической точки зрения. Но есть способ интереснее:
a ^= b ^= a ^= b;
который также меняет местами значения этих переменных.
Update: В комментариях подсказали грамотную ссылку (Bit Twiddling Hacks) по трюкам с битовой арифметикой.
]]>Лично я обычно на новых для меня языках люблю писать Решето Эратосфена в качестве “Hello, world!”.
Моя версия на Go.
Файл erato-go-bool.go
:
package main import "fmt" import "math" import "flag" func main() { var N int flag.IntVar(&N, "N", 100, "") flag.Parse() fmt.Printf("%d\n", N) seive := make([]bool, N) limit := int(math.Sqrt(float64(N))) + 1 for i := 2; i < limit; i++ { if !seive[i] { for j := i * i; j < N; j += i { seive[j] = true } } } count := 0 for i := 2; i < N; i++ { if !seive[i] { count++ } } fmt.Printf("%d\n", count) }
И первый вопрос, который приходит в голову - а насколько это быстро работает?
Некоторое время назад я уже писал, как использовал решето для тестирования STL’евского контейнера std::vector
на разных компиляторах.
Сейчас я провел похожее сравнение между Go, C++ и C.
Итак, первый кандитат - версия на Go с использованием типа bool
(см. выше). Второй - тоже на Go, но с использованием типа int
.
Файл erato-go-int.go
:
package main import "fmt" import "math" import "flag" func main() { var N int flag.IntVar(&N, "N", 100, "") flag.Parse() fmt.Printf("%d\n", N) seive := make([]int, N) limit := int(math.Sqrt(float64(N))) + 1 for i := 2; i < limit; i++ { if seive[i] == 0 { for j := i * i; j < N; j += i { seive[j] = 1 } } } count := 0 for i := 2; i < N; i++ { if seive[i] == 0 { count++ } } fmt.Printf("%d\n", count) }
Файл erato-cxx.cpp
:
#include <iostream> #include <vector> #include <cstdlib> #include <cmath> int main(int argc, char* argv[]) { int n = argc > 1 ? std::atoi(argv[1]) : 100; std::cout << n << std::endl; int sqrt_n = static_cast<int>(std::sqrt(static_cast<double>(n))) + 1; std::vector<TYPE> S(n, true); for (int i = 2; i < sqrt_n; ++i) if (S[i]) for (int j = i*i; j < n; j+=i) S[j] = false; int count = 0; for (int i = 2; i < n; ++i) if (S[i]) count++; std::cout << count << std::endl; return 0; }
Ну и для полноты картины версия на С:
Файл erato-c-int.c
:
#include <stdio.h> #include <stdlib.h> #include <memory.h> #include <math.h> int main(int argc, char* argv[]) { int n = argc > 1 ? atoi(argv[1]) : 100; int* S; int count; int sz = n * sizeof(*S); int i, j; printf("%d\n", n); long sqrt_n = sqrt(n) + 1; S = malloc(sz); memset(S, 0, sz); for (i = 2; i < sqrt_n; ++i) if (S[i] == 0) for (j = i*i; j < n; j+=i) S[j] = 1; count = 0; for (i = 2; i < n; ++i) if (S[i] == 0) count++; printf("%d\n", count); free(S); return 0; }
Ну и Makefile
для удобного запуска:
.SILENT: all: $(MAKE) run 2>&1 | tee log $(MAKE) parse-log run: go-bool go-int cxx-int cxx-bool c-int N ?= 100000000 go-bool: echo $@ 6g erato-$@.go 6l -o erato-$@ erato-$@.6 time -p -f %e ./erato-$@ -N=$(N) go-int: echo $@ 6g erato-$@.go 6l -o erato-$@ erato-$@.6 time -p -f %e ./erato-$@ -N=$(N) cxx-bool: echo $@ g++ -o erato-$@ \ -O3 -funroll-all-loops -fomit-frame-pointer \ -DTYPE=bool erato-cxx.cpp time -p -f %e ./erato-$@ $(N) cxx-int: echo $@ g++ -o erato-$@ \ -O3 -funroll-all-loops -fomit-frame-pointer \ -DTYPE=int erato-cxx.cpp time -p -f %e ./erato-$@ $(N) c-int: echo $@ gcc -o erato-$@ -lm \ -O3 -funroll-all-loops -fomit-frame-pointer erato-$@.c time -p -f %e ./erato-$@ $(N) parse-log: printf "%10s %10s %8s %5s\n" "Language" N Count Time ; \ (echo "------------------------------------") ; \ while read type ; do \ read N && \ read count && \ read time && \ printf "%10s %10s %8s %5s\n" $$type $$N $$count $$time ; \ done < log
Запускал я все это под Ubuntu 64-bit. Компилятор C и C++ - gcc 4.4.1. Компилятор Go - последний из официального репозитория.
Запускаем:
make N=100000000
и получаем следующее:
Language N Count Time
------------------------------------
go-bool 100000000 5761455 3.96
go-int 100000000 5761455 6.58
cxx-int 100000000 5761455 6.76
cxx-bool 100000000 5761455 2.20
c-int 100000000 5761455 6.47
Получается, что сделал всех С++ с использованием std::vector<bool>
для хранения массива. Затем идет Go тоже с типом bool
. А С, С++ с std::vector<int>
и Go с int
‘ом примерно равны.
Update: После экспериментов в комментариях выходит, что и на С, и на С++ можно добиться равного быстродействия, если использовать битовые поля. Просто в С++ это синтаксически проще, но не более того.
Посты по теме:
]]>Так как данная сфера деятельности активно развивается в англоязычном мире, то многие термины проще использовать без перевода, чтобы не потерять по дороге смысл.
Рассмотрение будет сфокусировано на техническую сторону вопроса с точки зрения компьютерной автоматизации процесса электронной торговли ценными бумагами. Для прояснения сопутствующих вопросов (в основном из общей экономической теории ценных бумаг) следует обраться к другим источниками.
Итак, для начала несколько простых определений.
Трейдинг - это торговля ценными бумагами. Соответственно, трейдинговая система (платформа) предоставляет автоматизацию этого процесса для различных участников торгов: для тех, кто продает и покупает, и для третьих лиц, предоставляющих различные сервисы и живущие на комиссию.
Надо отметить, что многие принципы электронной торговли повторяют то, как это происходило раньше, когда не было компьютеров. Но даже сейчас, в эру тотальной компьютеризации, в трейдинге весьма много ручной работы, причем несвязанной напрямую с принятием решения о покупке или продаже.
Ценная бумага - это объект торговли (в английском языке это называется “security”). Типов ценных бумаг очень много (акции, облигации, валюта и т.д.). Я не буду вдаваться в объяснение их сути (для этого следует обраться к литературе по экономике), так как с точки зрения компьютерной системы каждая ценная бумага - это всего лишь идентификатор. Например, “GOOG US Equity” - это акции Гугла на Нью-Йоркской фондовой бирже, “VOD LN Equity” - это акции Vodafone на Лондонской бирже, “CT2 Govt” - это двухлетние государственные облигации правительства США и т.д. Таких идентификаторов тысячи, и в целом большинство электронных систем торговли стараются соблюдать уникальность данных идентификаторов во избежание путаницы при операции между различными платформами.
Каждая система может иметь (и обычно имеет) внутреннюю систему идентификации ценных бумаг для удобства их учета.
Сделка (или trade) - это факт покупки/продажи ценных бумаг. Сделка обычно имеет несколько базовых характеристик: дата/время, идентификатор ценной бумаги, сумма за единицу и количество купленных/проданных единиц ценной бумаги. Данный квант информации обычно называется tick (или feed). Именно поток таких тиков, получаемых в режиме реального времени с различных торговых платформ, и является основным источником различных сводок, диаграмм, графиков, бегущих строк и т.д., описывающих “состояние фондового рынка”, по которым трейдеры принимают решение о том, что покупать или продавать, когда и почем.
Скорость поступления тиков может быть весьма высока (сотни или даже тысячи в секунду). Это объясняет, почему одним из важнейших показателей для трейдинговой системы является быстродействие. Чем быстрее обрабатываются входные данные, тем быстрее трейдер узнает об изменениях на рынке, и тем быстрее может принять решение. А для алгоритмического трейдинга (мы поговорим об это чуть ниже) - это вообще один из основных показателей. Это быстродействие может складывается из многих факторов: быстрота программного обеспечения, скорость информационных каналов (часто используются выделенные линии, а не публичные сети типа интернета), производительность аппаратного обеспечения и т.д.
Может так случиться, что из-за каких-то проблем в системе, из-за которой она работает очень медленно, может начаться попуск тиков (система не успевает их обрабатывать). В этом случаем требуется периодическая сверка обобщенных показателей, чтобы не отображать неверные данные.
Основные участники рынка ценных бумаг
Sell side (я оставлю этот термин без перевода) - это те, что ориентирован на перепродажу. Трейдеры - это типичные представители sell side. Обычно они не имеют своих активов. У них просто есть немного денег. В начале торгового дня они покупают что-то, а потом в течение дня могут перепродавать, покупать другое и т.д. В конце дня они обычно все стремятся распродать, даже если придется продать в убыток. Sell side обычно зарабатывает на быстрых дневных изменениях курсов, вызванных, например, неожиданными новостями.
Экономическая причина существования sell side - это, как в случае процессинговых систем, вопрос объема. То есть, нельзя просто так взять, и кому угодно подключиться к фондовой бирже, так как это стоит денег (депозиты, оборудование, сертификация, если потребуется и т.д.). Высокий ценовой ценз сделан для отсечения мелких клиентов (бирже не интересно возиться с клиентами, покупающими одну акцию в год). Последние должны будут пользоваться услугами более крупных трейдинговых фирм, и им уже придется платить комиссию.
Buy side - это “богатые” участники. Обычно это фонды, банки, страховые компании и т.д. По большому счету - это настоящие держатели ценных бумаг. Они их покупают помногу, с долгой перспективой, и обычно зарабатывают при росте цен (или теряют при их падении, такое тоже бывает) на длительном (несколько лет, например) интервале времени.
То есть в течение торгового дня sell side перепродает туда-сюда ценные бумаги от buy side, а к концу дня все снова фактически возвращается к buy side, и sell side имеет свою дневную разницу.
Конечно, такое разделение может быть весьма условным, и многие финансовые институты являются одновременно sell side и buy side. Но понимание этого различия весьма важно.
Рассмотрим некоторые важные части трейдинговой системы.
Мониторинг
Данная подсистема занимается получением различных входных данных о сделках (собственных и от других систем, новостей и т.д.) и предоставлением всего этого вала информации в удобном для анализа виде (таблицы, графики, статистика). Надо сказать, что новости являются очень важной частью, так как рынок, особенно такой подвижный, как фондовый, а особенно такой подвижный из-за его тотальной компьютеризации, реагирует мгновенно. С момента, как на сайте какой-то компании появился пресс релиз, например, о смене руководства, до начала активного движения цен ценных бумаг данной компании на рынке из-за этого пресс релиза проходят секунды. И тот, кто получил новость быстрее, тот и сумел среагировать и как результат - заработать.
Про новостные системы стоит сказать особо. Они состоят из нескольких уровней. После получения того или иного факта, на первом уровне новость ужимается в одно предложение из нескольких слов и тут же публикуется. Это позволяет для новостного провайдера выпустить новость очень быстро (буквально несколько минут), а для потребителя новости (например, трейдера), увидев новую строчку в новостной ленте (а не читать целый абзац) уловить суть и, если надо, начать действовать. Например, “ГАЗПРОМ снял генерального директора” или “Сбербанк объявил о банкротстве”. Далее новость передается на следующий уровень, где пишется уже абзац или два с более детальным описанием. Тут уже может пройти час или два. Если изначальная новость была бомбой, то многие будут ждать этого описания, а если нет, то возможно никто ее и не станет читать. Ну а затем уже на третьем уровне много позже могут написать детальную аналитику, и, например, включить мнения экспертов на влияние новости на те или иные рынки, ценные бумаги и т.д.
Системы учета позиций и истории сделок
Этот механизм позволяет трейдеру вести учет своей дневной деятельности. Фактически, это журнал его действий: купил это за столько, продал это тому-то. Кажущаяся на первый взгляд утилитарность и простота данной системы на самом деле ошибочна. Система учета позиций и истории сделок позволяет трейдеру моментально видеть, сколько и на сколько он продал и купил, и каковы его цены по сравнению с другими участниками рынка. Кстати, множество сделок до сих пор совершаются просто по телефону, и такие сделки надо где-то фиксировать. И немаловажно, на этом уровне происходит контроль ограничений самого трейдера. Например, данный трейдер не может торговать в день не более такой-то суммы, или что нельзя уходить в долг на такой-то ценной бумаге более чем на столько, или если трейдер вводит сделку более такой-то суммы, то запрашивается разрешения его начальника и т.д. Система контроля может быть очень и очень изощренной.
Алгоритмический трейдинг может также работать на этом уровне. Специальная программа, а не человек (или может человек, но который не управляет каждой отдельной сделкой, а запускает специальные скрипты, делающие за него эту работу), на основе информации с рынка принимает решения покупать или продавать. Программа может делать это очень быстро, гораздо быстрее человека, и, пользуясь очевидной логикой, когда что-то продают, то оно тут же дешевеет, а когда покупают - тут же дорожает, можно управляемо двигать цены на рынке, и тем самым иметь высоковероятностные предсказания о движении цены, и, как результат, вовремя (= выгодно) продать или купить.
Системы проведения сделок
Обычно, работа трейдера - это договориться о сделке. Электронно или по телефону - неважно. Проведение сделки обычно происходит после закрытия рынков. И этим занимается middle office. На данном этапе могут также проводиться дополнительные проверки активности трейдера.
Различные истории о трейдерах, нелегально что-то там наторговавших, обычно связаны именно с тем, что трейдер смог провести сделку без каких-то проверок, или нашел способ эти проверки обойти.
Автоматизация процесса тут обычно заключается в том, чтобы электронным образом разослать контрагентам информацию о проведенных сделках, получить ответы от них и подготовить данные для финансового отдела, который будет физически проводить расчеты с контрагентами.
Анонимная торговля
Первым способом торговли может быть, когда трейдер напрямую покупает (или продает) у конкретного контрагента (например, у своего постоянного партнера).
Альтернативным способом сведения покупателя и продавца может быть анонимная торговля. В этом случае покупателю (или продавцу) без разницы, кто его контрагент, так как его интересует только цена. Для автоматизации такой анонимной торговли есть специальные механизмы.
Устройство электронного рынка для анонимной торговли
Электронный рынок обычно устроен по следующей схеме. Есть такое понятие, как “Market depth”. Мне не очень нравится перевод “глубина рынка”, поэтому я оставлю термин без перевода. “Market depth” - это таблица лучшего спроса и предложения на рынке для выбранной ценной бумаги. Выглядит она примерно так:
Bid Offer
---------+----------
Ц1 х К1 | Ц5 х К5
---------+----------
Ц2 х К2 | Ц6 х К6
---------+----------
Ц3 х К3 | Ц7 х К7
---------+----------
...
---------+----------
Ц4 х К4 | Ц8 х К8
---------+----------
Колонка Bid - это цены и объемы того, что люди хотят купить (список текущего спроса). Эта колонка отсортирована по убыванию цены:
Ц1 > Ц2 > Ц3 > ... > Ц4
То есть физический смысл этой колонки - это показать лучшие (= наибольшие) с точки зрения потенциального продавца цены (и возможные объемы) по данной ценной бумаге.
Допустим, ячейки “Ц1 х К1” пока нет, а на вершине колонки Bid находится “Ц2 х К2”. Я, как участник рынка, говорю «я хочу купить ценную бумагу N по цене “Ц1” в количестве “К1”». Так как моя цена больше цены “Ц2”, то моя заявка становится на вершину колонки.
Колонка “Offer” (или иногда ее называют “Ask”) - это цены и объем того, что люди хотят купить (список текущих предложений). Эта колонка отсортирована по возрастанию цены:
Ц5 < Ц6 < Ц7 < ... < Ц8
Физический смысл этой колонки - это показать лучшие (= наименьшие) с точки зрения потенциального покупатели цены (и объемы) по данной ценной бумаге.
Далее начинается самое интересное. Как известно, спрос удовлетворяет предложение и наоборот. Так вот данная таблица и является механизмом сопоставления спроса и предложения.
Если я прихожу на рынок и говорю «я хочу купить ценную бумагу N по цене “Ц1” и количестве “К1”», система берет таблицу “Market depth” для ценной бумаги N и просматривает колонку “Offer”.
Если какая-то цена совпадает с моей (например, “Ц6”), то система автоматически удовлетворяет мою заявку и продает мне нужное количество ценных бумаг N по цене “Ц1” (= “Ц6”) и в количестве “К1”. И ячейка “Ц1 х К1” не появляется в колонке спроса “Bid”, так как мой спрос удовлетворен. Если мое запрошенное количество “К1” равно “К6”, то строка “Ц6 х К6” удаляется из колонки предложений “Offer”. Если мое количество “К1” меньше “К6”, то строка “Ц6 х К6” остается в колонке, но из “К6” просто вычитается “К1”. Ну а если же мое количество “К1” больше “К6”, то строка “Ц6 х К6” удаляется из колонки “Offer” (это предложение полностью удовлетворено), а в колонке “Bid” появляется моя строка “Ц1 х К1”, где “K1” отображается за вычетом “K6”, то есть “Ц1 х K1-K6”.
Аналогично, для продажи. Если я прихожу на рынок и говорю «я хочу продать ценную бумагу N по цене “Ц6” и количестве “К6”», система берет таблицу “Market depth” для ценной бумаги N и просматривает колонку “Bid”.
Если какая-то цена совпадает с моей (например, “Ц2”), то система автоматически удовлетворяет мою заявку и покупает у меня нужное количество ценных бумаг N по цене “Ц6” (= “Ц2”) и в количестве “К6”. И ячейка “Ц6 х К6” не появляется в колонке спроса “Offer”, так как моё предложение удовлетворено. Если мое запрошенное количество “К6” равно “К2”, то строка “Ц2 х К2” удаляется из колонки предложений “Bid”. Если мое количество “К6” меньше “К2”, то строка “Ц2 х К2” остается в колонке, но из “К2” просто вычитается “К6”. Ну а если же мое количество “К6” больше “К2”, то строка “Ц2 х К2” удаляется из колонки “Bid” (этот спрос полностью удовлетворен), а в колонке “Offer” появляется строка “Ц6 х К6”, где “K6” отображается за вычетом “K2”, то есть “Ц6 х K6-K2”.
По сути, таблица “Market Depth” отображает список пока еще неудовлетворенных спросов и предложений. Как только находится для спроса парное предложение или наоборот - между ними производится сделка (trade), и эта пара удаляется из таблицы (одна из сторон может после сделки таки остаться в таблице “Market Depth”, если, как описано выше, не было полного совпадения по количеству).
Обычно “Market depth” отображает в среднем от 10 до 100 уровней цен по спросу и предложению. Больше нет особого смысла нет.
Кроме того, каждая ячейка данной таблицы может раскрываться списком заявок по цене. Что это значит? Например, ячейка “Ц1 х K2” может быть агрегированным по цене показателем, то есть количество “К1” является предложением не единственного покупателя, а многих (несколько покупателей могут хотеть купить по цене “Ц1”), но так как для продавца неважно, кому именно продавать (цена то все равно одна и та же, и система в целом анонимна), по умолчанию эта детализация может не отображаться.
Механизм сопоставления спроса и предложения по таблице “Market Depth” лежит в основе анонимного трейдинга, и так называемый алгоритмический трейдинг может быть построен на программировании логики автоматизированной торговли, основанной на анализе данных таблицы “Market Depth”.
Для алгоритмического трейдинга система предоставляет программный интерфейс (API), который может быть, как вариант, скриптовым языком, например, Lua, с набором функций для просмотра таблиц “Market Depth” и для помещения в систему заявок на покупку или продажу. Логика же программируется автором скрипта.
Интересно, что порой трейдеры хранят свои скрипты в большой тайне, и даже в случае технических неполадок (например, скрипт работает не как ему положено), отказываются их показать техслубже, хотя решить техническую проблему при этом требуют.
Мы рассмотрели некоторые из основных понятий, терминов и механизмов трейдинговых систем на начальном уровне. Многие из описанных механизмов в реальности сложнее и гибче, но основные принципы от этого не меняются.
Посты по теме:
]]>sizeof
в С++ для сложных составных типов (структур и классов) и сходу не вдаваться в детали выравнивания – надо запомнить, что sizeof
возвращает число, равное разности адресов двух соседних элементов массива, хранящего экземпляры вашего типа.
]]>Простейшее решение выглядит так:
#include <iostream> #include <vector> int main(int argc, char* argv[]) { int a[] = { 1, 2, 3, 4, 5 }; std::vector<int> v(a, a + 5); for (int i = 0; i < v.size(); ++i) { std::cout << v[i]; if (i < v.size() - 1) std::cout << ", "; } std::cout << std::endl; return 0; }
Условие в теле цикла решает поставленную задачу, но контейнеры лучше обходить через итераторы, поэтому следующая попытка может выглятеть так:
#include <iostream> #include <vector> int main(int argc, char* argv[]) { int a[] = { 1, 2, 3, 4, 5 }; std::vector<int> v(a, a + 5); for (std::vector<int>::const_iterator i = v.begin(); i != v.end(); ++i) { std::cout << *i; if (v.end() - i > 1) std::cout << ", "; } std::cout << std::endl; return 0; }
Но такой подход не самый верный, ибо итераторы далеко не всех контейнеров поддерживают операцию вычетания. Например, при использовании std::list
вместо std::vector
будет ошибка компиляции (как, кстати, и для первого примера, но по другой причине). Поэтому правильнее было бы написать:
#include <iostream> #include <vector> int main(int argc, char* argv[]) { int a[] = { 1, 2, 3, 4, 5 }; std::vector<int> v(a, a + 5); typedef std::vector<int>::const_iterator iterator; for (iterator i = v.begin(); i != v.end(); ++i) { std::cout << *i; if (std::distance<iterator>(i, v.end()) > 1) std::cout << ", "; } std::cout << std::endl; return 0; }
Шаблонный класс std::distance
умеет рассчитывать расстояние между итераторами, и даже для тех, которые не поддерживают операции сложения и вычетания. Для таких итераторов будет делаться пошаговый обход от одного к другому для подсчета расстояния. На первый взгляд получается, что вычислительная сложность такого простого цикла будет уже не линейной, а квадратической. Еше надо таскать за собой описание типа дважды — чтобы создать итератор цикла и экземпляр std::distance
. Например, Visual Studio 2008 требует указывать тип итератора для шаблона std::distance
и не может “угадать” его из параметров (другие компиляторы могут вести себя иначе). Получается, на ровном месте навернули какую-то ерунду.
Но есть весьма элегантный способ, который позволяет и использовать итераторы, и сохранить линейную сложность алгоритма для контейнеров, которые не умеют эффективно вычислять расстояние между элементами (например, std::list
), и писать красиво и компактно:
#include <iostream> #include <vector> int main(int argc, char* argv[]) { int a[] = { 1, 2, 3, 4, 5 }; std::vector<int> v(a, a + 5); for (std::vector<int>::const_iterator i = v.begin(); i != v.end(); ++i) { std::cout << *i; if (i != --v.end()) std::cout << ", "; } std::cout << std::endl; return 0; }
Трюк с оператором --
позволяет эффективно проверить на последний элемент контейнера.
В целом не планируется заменить основной репозиторий, но хочется несколько оживить процесс внесения новых патчей. Для начала я туда заправил свой мини-патч для вывода результатов тестирования похожим на Google Test образом, и несколько багфиксов.
Все-таки распределенные системы контроля версий более удобны, особенно когда много разрозненных разработчиков, которые “точат” свои куски.
Посты по теме:
]]>100-31
далеко не означает 100 долларов и 31 цент, или 100-127
вообще имеет мало смысла, так как в одном долларе всего 100 центов, а не 1000, и нет необходимости резервировать под дробную часть три знака после запятой.
Вся хитрость тут в том, что это не привычная десятичная запись. Например, 100-31
в десятичной форме равно 100.97265625
, а 100-127
соответствует 100.40234375
.
Итак, данный формат записи дробных чисел называется thirty seconds
или 32nd
. Для визуального удобства и явного отличия от десятичной формы вместо точки в качестве разделителя используется маленькая черточка. А само число имеет в общем следующий формат:
AAA.XXY
где AAA
- это целая часть числа, имеющая такой же смысл, как и в десятичной системе. XX
- это количество 1/32
-х долей от дробной части, а Y
- это количество восьмушек (1/8)
в последней 1/32
доле. Несмотря на туманное описание, формула перевода числа AAA.XXY
в формате 32nd
в десятичный формат весьма проста:
D = AAA + (XX + Y * 1/8) * 1/32
или
D = AAA + XX * (1/32) + Y * (1/256)
то есть для числа 100-127
ААА=100
, XX=12
, Y=7
, поэтому:
D = 100 + 12/32 + 7/256 = 100.40234375
Чтобы формула была корректной, XX
может принимать значения только от 00 до 31, а Y
от 0 до 7. Также при записи Y
число 4 может быть заменено на +
, а 0 на пробел. То есть 100-31
в полной форме записи равно 100-310
, а 100-12+
эквивалентно 100-124
.
Видно, что в трех дробных разрядах кодируется не 1000 долей, как в десятичной системе, а только 256 (32 * 8
).
Итак, еще раз: если написано 100-12+
, то это 100.39062500
в десятичной системе.
Формула обратного перевода из десятичного представления в формат 32nd не многим сложнее. Пусть D
десятичное число:
A = TRUNC(D)
XX = TRUNC((D - A) * 32)
Y = ((D - A) * 32 - XX) * 8
TRUNC
- это функция взятия целой части.
Если Y
равно 0, то можно этот разряд не писать, а если 4, то можно заменить на +
.
Компонента Y
должна получиться обязательно целочисленной. Иначе, наличие дробной части у Y
- это признак, того, что исходное десятичное число D
не имеет отображения в формат 32nd (только 256 значений дробной части из всех 1000 возможных могут иметь соответствие в формате 32nd).
Как бы ни причудливо не выглядел подобный способ записи денежных сумм, именно его используют американские трейдеры (не путать с рейдерами), при ведении торгов государственным облигациями. Могу предположить, что это просто наследие времен, когда далеко не все знали дробные десятичные числа, а запись частей целого в виде натуральных дробей гораздо ближе к натуре человека. Разделить кучку на две, три и т.д. части может даже ребенок, необученный десятичным дробям.
Формат странный, но знать его приходится.
]]>Итак, 10 распространенных заблуждений.
1. Количество денег хранится на самой карте.
На обычной кредитной или дебетовой карте (даже если она с чипом) нет никакого счетчика денег. Карточка - это просто идентификатор. Бывают исключения в виде особых дополнительных приложений-кошельков на картах с чипом. Обычно это могут быть скидочные программы, виртуальные деньги (например, литры бензина) и т.д. В общем, что-то несвязанное напрямую с обычным использование карты. Но такие особые приложения принимаются только в торговых точках, участвующих в поддержке этого конкретного типа карт.
2. Каждый, кто хочет принимать платежи через банковские карточки, может подключиться напрямую в Визу, Мастеркард или любую другую международную систему.
Нельзя просто так кому угодно подключиться напрямую в Визу или Мастеркард. Это могут делать только богатые банки или независимые процессинговые центры, так как нужно особое оборудование, немалые страховые счета, сертификация по безопасности и много других “мелочей” (даже не каждый банк может себе такое позволить). Все остальные желающие принимать карты пользуются их услугами.
3. Банкоматы или терминалы для платежей подключены прямо в Визу или Мастеркард.
Крупные международные платежные системы не держат своих банкоматов или платежных терминалов. Любой банкомат или терминал обязательно принадлежит какому-то банку, который в свою очередь либо сам, либо опосредовано (см. п.2) подключен в платежную систему.
4. У меня “на карте” $200. Это все, что я могу потратить.
Остаток на счету и сумма, которую можно потратить в день с карты, сильно между собой несвязаны. Конструктивнее говорить о дневном лимите по карте. Дневной лимит зависит от множества факторов, и может быть как меньше остатка на счету, так и больше. Например, даже если на счету миллион, вам врядли дадут снять в банкомате более нескольких тысяч в день (и это не ограничение банкомата как устройства). И наоборот, но если вы VIP-клиент, у которого обычно на счету миллионы, а сейчас вы в казино и все уже продули, то после звонка в банк, в индивидуальном порядке какой-то из высоких менеджеров может дать команду установить лично для вас нужный лимит, чтобы вы смогли-таки расплатиться. В этом случае банк берет на себя ответственность, что вы ему потом все отдадите.
5. При использовании карты ПИН-код проверят сам банкомат или платежный терминал.
В подавляющем количестве случаев любое использование карты подразумевается соединения с банком, выдавшим карту. Если вы суете карту Сбербанка в банкомат в Австралии, то разрешение на выдачу денег все равно будет запрошено напрямую из Сбера прямо на ваших глазах. Все это потому, что ПИН-код может быть проверен только банком, выпустившим карту. Исключением являются карты с чипом. Такие карты могут сами проверить ПИН (так как сама карта-чип - это миникомпьютер, умеющий выполнять крипто-функции). Также, иногда для использования карты для оплаты покупки (а не снятия наличных), торговая точка может не связываться с авторизационным центром для каждой покупки, если сумма меньше какого-то лимита. Это может быть актуально для небольших сумм, когда сумма покупки меньше стоимости сессии обмена по электронному каналу. Так как суммы невелики, и иногда применяются дневные счетчики по картам, авторизованным таким образом, то и риски нарваться на большие потери из-за мошеннических операций тоже невелики.
6. На магнитной полосе записан ПИН, который может “украсть” любой сотрудник банка, стоит только отвернуться, пока твоя карточка у него в руках.
На самом деле, на магнитной полосе записана крипто-свертка ПИНа и номера карты, полученная при помощи криптографического ключа, который хранится внутри суперохраняемой железяки в банке. То есть с помощью данных с магнитной полосы можно только проверить ПИН, да и то, если знать секретный ключ. Обычно в качестве алгоритма шифрования используется 3DES. «Суперохраняемая железяка» - это аппаратное устройство для хранения ключей и проведения крипто-операции на их основе. То есть после начального ввода ключей (персонализации) в этой устройство они никогда не передаются вне физического корпуса в чистом виде.
Помимо серьезных мер по физической охране этих устройств, сами они имеют защиту от проникновения. Например, если попытаться открыть его корпус для подключения «сниффера», то все ключи буду автоматически стерты.
Интересна методика начального ввода ключей. Например, реален вот такой сценарий. Выбирается N сотрудников безопасности банка, например, 3 (в идеале, они даже знать друг друга не должны лично). Каждый генерирует вариант ключа и никому его, конечно, не показывает. Затем, они по очереди заходят в помещение, где стоит оборудование по хранению ключей, и вводят каждый свой ключ. Затем, когда все ключи введены, устройство делает операцию XOR между ними, и это сохраняет внутри себя в качестве ключа. Получается, что ключ не знает никто вообще. И чтобы его восстановить, надо получить исходные компоненты от каждого из тех N сотрудников безопасности, которые обязаны заботиться о конфедециальном их хранении.
Как я уже писал, в безопасности полумер нет, и подобные административные меры нужны, когда кончается сила криптографии, и начинается человеческий фактор.
Важное замечание: никто из сотрудников банка, никогда, ни при каких условиях не будет спрашивать вас ПИН. Но если б вы знали, сколько раз из десяти, клиенты, звонящие в банк, на вопрос оператора об их секретном слове (которое задавалось при открытии счета), говорят ПИН.
7. При совершении покупки деньги сразу попадают напрямую со счета клиента на счет магазина.
Обычно реальный обмен деньгами (пусть и электронными), происходит в конце рабочего дня. А в момент самой покупки производится только блокировка суммы из доступного лимита (см. п.4). Списание же обычно происходит через несколько дней, когда до банка-владельца счета дойдет финансовое представление от банка, через чей терминал был проведен платеж.
8. Сумма, написанная на вашем чеке при оплате картой, в точности будет списана с вашего счета.
На самом деле, сумма, списанная при авторизации, может отличаться существенно от суммы, которая списана по финансовой транзакции. Особенно это проявляется при оплате аренды машин и оплате гостиниц, так как эти торговые точки могут “вдогонку” списать дополнительные расходы (пример, недостачу бензина, или неоплаченный мини-бар). Но не только этим типам торговых точек также разрешено увеличивать или уменьшать конечную сумму.
Также, сумма, заблокированная при авторизации, может отличаться от суммы, списанной со счета, в случае, если валюта счета отличается от валюты операции, так как реальное списание средств со счета происходит через 1-2 дня, а за это время курс конвертации может измениться.
9. Сумма, заблокированная на счету при оплате картой, так или иначе спишется с моего счета.
Сумма, заблокированная при авторизации, может никогда быть не списана со счета. Через 10 (для банкомата) или 45 (все остальные терминалы) дней без прихода в ваш банк финансового подтверждения операции из платежной системы она будет разблокирована. Это и “хорошо” и “плохо”. Это “хорошо”, когда вы сделали операцию, от которой хотите тут же отказаться. Сразу после операции вы звоните в банк, объясняете оператору причину отказа, и если она разрешенная, то операция “отменяется”, и блокировка может быть снята. В этом случае если вдруг на операцию придет финансовое подтверждение от торговой точки (через пару дней), то сам банк будет разбираться с ним без вашего участия (и ваших денег). Это “плохо”, когда вы таки подождали день-два, и финансовое подтверждение уже пришло в банк до вашего звонка, тогда “откатить” операцию будет уже сложнее. Банк будет вынужден начать официальное разбирательство по этому случаю, которое может длиться эти 45 дней. В течение этого времени сумма покупки может оставаться заблокированной.
10. Владельцы дебетовых (а не кредитных) карт не могут оказаться “должны банку”.
Как уже говорилось в п.4 — логика авторизации покупки основана не на реальной сумме на счету, а на дневных лимитах, то как и для кредитных карт, так и для дебетовых, можно “залезть в минус”, если банк ставит дневные лимиты, немного превышающие остаток на счету даже для дебитных карт.
Надеюсь, эта информация поможет вам избежать некоторых неприятных сюрпризов при использовании пластиковых карточек.
Посты по теме:
]]>Меня всегда интересовал вопрос — если администратор замечательного сервиса всех времен и народов «Мейл.ру» сможет узнать пароль ящика пользователя (не удивлюсь, если они хранят пароли в чистом виде) – какова вероятность, что у пользователя с аналогичным именем, скажем, на почте Яндекса, будет точно такой же пароль — более 90% или нет? (А сколько у вас «кросс»-паролей? 2, 3 или больше?)
Вообще, проблема использования одинаковых паролей на публичных сервисах очень недооценена, увы. Вспомните, сколько всяких форумов, онлайн-магазинов, социальных сетей и т.д. могут при восстановлении пароля на аккаунт прислать вам пароль по почте? Их много. Это значит, что ваш пароль где-то хранится в чистом виде и может быть сдан на сторону недобросовестным администратором или подсмотрен из письма, адресованного вам с напоминанием текущего пароля.
Конечно, правильно организованная система никогда не хранит пароли в чистом виде. В простом случае хранится свертка (желательно, крипто свертка под секретным ключом, которые живет в аппаратном устройстве, а не на диске). И если же надо хранить таки сами пароли, например, пин-коды в банке, то обычно в базе данных хранятся пароли (пин-коды) в зашифрованном виде, а их проверка и печать пин конвертов делается специальными аппаратными устройствами, в которые уже горе-администратор шаловливыми ручками не залезет. Например, в случае вскрытия своего опломбированного корпуса такое устройство стирает все ключи внутри себя.
Интересно, современные почтовые системы типа Gmail или Яндекс используют подобные устройства?
Факт, что в сейчас мире электронная почта несет все больше и больше конфиденциальной информации (электронные платежи, переписка с банком, с работодателем и т.д.). Конечно, грамотное использование технологий (электронная подпись и шифрование писем) и немного здравого смысла (не высылать полные номера банковских счетов и карт, например) решают большинство проблем. Но всего лишь единственный недобросовестный онлайн сервес, сдавший «налево» ваш пароль от очередной социальной сети «…ясьники» в случае его совпадения с паролем от вашего основного почтового ящика, может доставить вам множество часов нервотрепки, когда вы будете вспоминать – не было ли у вас там чего-то особо секретного в том онлайновом почтовом ящике, который у вас «увели».
Пример из личного опыта.
Пару лет назад у меня «увели» почтовый ящик на «Мейл.ру» (с тех пор у меня устойчивый рвотный рефлекс при упоминании этого сервиса). Я не «восторженная блондинко»™ с паролем «1234» на все и имею здравый смысл. Пароль там был «сильный», и единственный способ его потери – это сдача «налево» кем-то из «администрации». Цель? Не могу даже представить, для чего можно это сделать (продать однофамильцу красивый адрес? бред). Но месячная переписка с суппортом и менеджментом сервиса не привели к успеху – ни то, что я мог показать им всю почтовую базу за пять лет и переписку с новым «владельцем» ящика, который предлагал мне его продать назад – ни к чему не привели. Ящик мне не вернули. Благо использовался он только для пересылки, и писем в онлайне не было. Но я потратил недели, чтобы вспомнить все места, где я регистрировался по тому ящику, и экстренно заменить украденный адрес и пароль (пока это не сделал новый владелец через восстановление пароля на e-mail). Не думаю, что это была целенаправленная атака на меня именно, но масштаба проблемы это не уменьшило ни на йоту. Забавно, были сервисы, например, «Аромат.ру», в котором имя аккаунта был сам e-mail, поэтому единственный выход был — это удалить аккаунт на Аромате. А были сервисы, где просто нет функции удаления аккаунта (зачем кто-то будет удалять потенциальный товар, который можно продать спаммерам?). В общем, я кое-как все подчистил, но было очень, очень противно. Очень схожее ощущение, когда тебя ограбили. Так что до сих пор от души желаю «Мейл.ру» скорого разорения.
После истории украденным ящиком, я переосмыслил методу создания, хранения и использования паролей.
Главный принцип — никогда не использовать повторяющихся паролей.
Легко сказать — сложно сделать. Вряд ли кому-то комфортно помнить более 5-6 паролей. Придумывать же систему генерации паролей в уме — тоже не выход. Современный средний человек пока в уме едва ли посчитает крипто- или хеш- функцию (а только им можно доверять), а иначе мы приходим к подходу «авось никто на догадается», который может больно ударить в спину.
Единственный выход — электронный хранитель паролей.
Подобных программ великое множество. У меня были основные критерии:
Почему open-source? Факт наличия открытых исходников дает более менее уверенность, что в программе нет «закладок», отсылающих ваши пароли по интернету, «системных» паролей по умолчанию (так как спецслужбы этого могут требовать), и вообще – популярный open-source – это значит, что код отсмотрен сотней другой внимательных глаз.
Кроме того, в хранилище конфиденциальных данных ценность каждого бита информации крайне высока, поэтому надежность хранилища тоже имеет большое значение.
Мой выбор пал на KeePass. Все три критерия великолепно выполнялись, а список платформ, под которые есть реализации, впечатляет.
Почему удобно иметь версии под разные платформы? А чтобы один и тот же секретный файл можно было бы открыть и в Windows, и в Linux или прямо на iPhone или Андроиде, если ты в пути.
Что конкретно делает эта программа, и почему это правильно?
Программа создает файл-хранилище, зашифрованный паролем. Так как используются современные алгоритмы шифрования, то взломать файл его можно разве что методом «грубой силы» полным перебором возможных паролей или по словарю, или методом паяльника. Если пароль «сильный», а не 1234, то в реальности остается только паяльник, но защита от паяльника уже вопрос не компьютерный, а организационный. Поэтому если пароль забыть — с данными можно попрощаться. KeePass имеет приятную дополнительную возможность, когда для открытия хранилища нужен не только пароль, но и секретный файл-ключ (или только он сам без пароля).
Внутри зашифрованного файла можно создавать записи. Каждая запись имеет имя и набор полей — имя, e-mail, url, пароль и поле для произвольных текстовых данных. Записи можно организовывать в группы с помощью простой древовидной структуры. Также есть контекстный поиск по записям.
Поле пароля — особое. По умолчанию оно отображается звездочками. Это удобно, если открытые окно программы случайно увидит кто-то посторонний. KeePass может по команде поместить пароль в буфер обмена на 10 секунд, чтобы вы успели переключиться в окно браузера сделать вставку пароля в форму. По истечении этого времени пароль будет стерт из буфера обмена. Таким образом, паролем можно воспользоваться, даже не видя его.
А теперь ключевая функция программы — генерация паролей. Допустим, вы регистрируетесь на каком-то сайте. Придумали логин, ввели e-mail, и надо вводить пароль. Вы нажимаете в KeePassе кнопку «Сгенерировать пароль» (его длину и алфавит используемых символов можно менять), и получаете новый случайный пароль у себя в окне KeePass’а (напомню, по умолчанию он отображается звездочками). Затем через буфер обмена вы переносите новый пароль в форму регистрации. При такой схеме, вы можете даже сами и не знать, какой там пароль.
Я зарегистрирован на десятках разных форумах, и я не только не помню свой пароль от них, я его даже и не знаю. Если надо, я его перетаскиваю из окна KeePass не глядя. Конечно, есть ключевые пароли: от вашего основного ящика, мессенджера и т.д., которыми пользуешься часто, и их удобно держать в голове. Но их должно быть два-три. Они также должны абсолютно разными и «сильными», но запоминаемыми. Остальной же регистрационный мусор стоит хранить не в голове, а в программе (например, в KeePass), тем самым не подвергая основные пароли опасности.
Файл с паролями и саму программу для их чтения можно носить на флешке, чтобы на любом компьютере можно было заглянуть в свои пароли, если что. Однозначно, основной пароль на зашифрованное хранилище должен быть «сильным». Тогда можно не бояться, у вас его украдут вместе с флешкой.
Если у вас есть портативное устройство (например, телефон с J2ME, Windows Mobile, iPhone или Android), то доступ к паролям можно получить прямо на ходу, если периодически сбрасывать на устройство последнюю версию зашифрованного файла.
У Андроида есть отличная вещь: разработчик приложения декларирует в манифесте какие ресурсы нужны его приложению (сеть, записная книга, флеш-карта, GPS и т.д.). Все остальные ресурсы приложению будут заблокированы. Этот манифест показывается тебе при установке приложения, и сразу видно, чем ты рискуешь. Было бы странно, если бы при установке KeePass на Google Nexus One мне бы сказали, что приложение будет ходить в интернет. Но KeePass запросил только доступ к флеш-карте для хранения файла с паролями.
KeePass хорош для хранение небольшого объема данных. А если счет идет уже на мегабайты или гигабайты, нужен другой подход.
Кроме KeePass я активно последние несколько лет использую TrueCrypt для создания зашифрованных дисковых томов. Удобно, например, на рабочем компьютере создать место, куда стопроцентно никто, кроме вас не заглянет. Кроме того, такие шифрованные «диски» удобно переносить с места на место и бэкапить. TrueCrypt, конечно, open-source, не требует предварительной установки в систему (можно просто носить на флешке) и существует под Windows и Linux (можно лазить за защищенный контейнер из любой из этих систем).
Не стоит пренебрегать защитой собственных конфиденциальных, которые приходится хранить в электронном виде (работать без паролей или со слабыми паролями, надеяться на принцип «никто не угадает», например, какая у меня тут маска подсети и использовать WEP, взламываемый за минуты, вместо WPA, не блокировать консоль компьютера, когда отходишь и т.д.). Все хорошо до первой засады, цена которой может быть очень высокой.
]]>Цена вопроса (без контракта): 529$ аппарат, 19$ британская зарядка, 30$ доставка. Итого 578 американских буказоидов.
Я давно являюсь фанатом Андроида, и после G1 и G2 я наконец решил купить сам аппарат, а не только играться на эмуляторе. Тем более Nexus — это официальный аппарат именно от Google.
Сразу скажу, что я пока не рассчитываю, что Nexus сразу полностью заменит мне айфон. Очевидно — аппарат новый, новый Андроид 2.1, так что скорее всего будут глюки (и они есть), но в качестве “игрушки для взрослых” — в самый раз (прошлой игрушкой у меня был Lego NXT), и так как платформа не в пример Эпплу открытая и свободная, обновления, новая информация, и вообще активность разрабочиков не заставит себя ждать (по крайней мере хочется в этого верить).
Вместо подробного отчета с расписыванием размеров, веса и т.д. просто приведу моменты, на которые обратил внимание, как программист (тем более, что есть официальный 3D тур по аппарату.
Что дают в комплекте.
Сам аппарат, съемный аккумулятор (не могу поддержать восторгов на эму тему, так как за два года пользования неразборным айфоном ни разу не захотелось его открыть и поменять аккумулятор, хотя он более одного дня не работает), зарядка от сети (американская и опционально плюс за 20$ британская), проводная гарнитура с микрофоном и кнопкой, кабель USB-to-MiniUSB (боже! стандартный разъем mini-USB), приятный чехол с логотипом Андроида, mini-SD карта на 4GB, инструкция в виде небольшой картонки, мини книжка с гарантией и условиями.
Забавно, что при оформлении заказа можно “персонализировать” аппарат путем нанесения надписи из двух строк на заднюю панель аппарата. Я ввел надпись на русском, и в заказе она выглядела нормально, но вот на в реальности на аппарате каждая русская буква представлена символом ?
— ну не взял их принтер русский шрифт на грудь. Не беда, это в своем роде тоже уникальная надпись.
Вынул я симку O2 из айфона и заправил в Нексус. В отличии от айфона, разъем симки находится под аккумулятором, и там же находится карточка mini-SD. Аппарат завелся. Грузился чуть дольше, чем айфон.
Первое, что он меня спросил — это гугловский аккаунт. После этого спросил пароль на wifi и затем моментально подхватил все из гугловского аккаунта (почта, календарь, контакты).
Настройки EGDE/GPRS пришлось руками вбивать из айфона, так как аппарат, конечно, был не в курсе о параметрах O2.
Gmail сделан отдельным приложением, заточенным только под него. Работает в фоновом режиме. Для традиционной почты (POP3, IMAP и т.д.) есть приложение Mail. Пользоваться очень удобно.
Google Chat работает также в фоне и показывает входящие сообщения моментально. Как я понял, тут нет никаких проблем, чтобы приложение работала в фоне, и так делают многие приложения.
Я как-то и не ожидал, что в первых версиях прошивки будет русификация. Но русские шрифты и язык для интерфейса таки оказались. Русской же клавиатуры не было. Ну я было уже приуныл и решил, что подожду обновления прошивки, но тут пришла мысль — а не сходить ли мне на Google Market (аналог Apple Store). И первое бесплатное приложение, которое я поставил — это она из русских клавиатур. Теперь я могу писать на русском.
Еще я бесплатно поставил (играясь вчера пару часов): англо-русский словарь (о да!), распознавалку штрих-кодов (фоткаешь штрих-код, и она находит в сети товар), эмулятор игры “Ну погоди!” (там, где волк яйца ловит с четырых полок — это угар!), клиент твиттера, эмулятор терминала (можно telnet’иться на сам Nexus), карту метро и нотификатор состояния его линий (для лондонского метро это актуально). Пока все. Понимаю, набор странный, но это только начало.
Поясню: для Андроида свободы создания приложений гораздо больше, чем для айфона. Можно написать даже свое приложение, которое будет реализовывать телефон как таковой, на замену стандартному.
В качестве аналога iTunes Store предлагается Amazon Store. Работает похоже (можно послушать и купить). Богатство ассортимента я не сравнивал пока (хотя догатываюсь, i-кто победит).
Явные пройгрыши айфону.
Тач-скрин не поддерживает так называемое мульти-нажатие, к которому так привыкаешь в айфоне (очень удобно для zoom’а). В Нексусе zoom сделан просто — делаешь двойной клик на экран, и возникают полупрозачные кнопки “+” и “-”. Кстати, надо отметить, что браузер в Nexus мне понравился больше, чем в айфоне, несмотря на отстутствие мульти-тача для увеличения/уменьшения. По умолчанию отображается сайт по всей ширине (горизонтальная ориентация, конечно, тоже есть), и при двойном клике на экран браузер каким-то удивительным образом подбирает размер шрифта для комфортного просмотра.
Еще одно явное неудобство — айфон гораздo умнее плане автоматического отображения виртуальной клавиатуры, когда на экране появляются поля ввода. В Нексусе надо практически всегда вызывать ее принудительно, кликая на поле ввода. Может это поведение можно как-то изменить в настройках — пока не нашел.
Также интересная деталь: в айфоне количество настроек в целом (меню Settings, например) разительно мало, по сравнению с Windows Mobile (скажите, кто не употреблял матерных слов, пытаясь настроить GPRS или Dial-up в смартофоне Windows?) — дают настраивать только что, реально надо настраивать. Так вот: в Нексусе настроек еще меньше.
Замеченные реальные глюки.
Пока я нашел только один — самопроизвольное переключение с wifi на EDGE (я уже читал, что это многими замеченный глюк). Лечится выключением и включением wifi (благо есть просто иконка на десктопе). Будем ждать обновления прошивки.
Пока все. На этом закончились мои первые несколько часов наедине c Nexus.
В целом вывод такой: возможности заменить им айфон пока точно нет, но степень интеграции с гугловскими сервисами и открытось платформы для разработки очень подкупают. Так что для не-фанатов гугла — может имеет смысл подождать, но для любителей гаджетов и поклонников Андроида — отличная игрушка, обеспечащая вам достаточный уровень красноты в глазах.
Постараюсь и далее рапортовать моих интересных находках в Google Nexus.
]]>Но сейчас не об этом. Дум сразу имел классную особенность — записывать игровые сессии для последующего просмотра. Это порой очень увлекательное зрелище, особенно когда играет мастер или мастера. А самая главная штука — это способ записи. При записи игровой сессии записываются не кадры видеоизображения, а просто последовательность действий игрока — зажатия на клавиатуру и движения мыши или джойстика. Во-первых, такая запись занимает очень мало места, во-вторых — это “чистая” запись, исключащая видеомонтаж, так как для проигрывания можно использовать свою копию игры, которая заведомо нормальная.
Едем дальше. В любой игре подобного толка есть новички (они же “мясо”), есть сильные игроки (я!), а есть мега-монстры (в случае с Думом — “дум-годы”). Очевидно, что сессии игры, записанные такими спецами смотрятся как увлекательное кино. Уровень, который ты проходишь минут за 5 он проходит за 5 секунд. Всю игру, на прохождение всех уровней которой у тебя могут уйти недели, он проходит за 15 минут на максимальной сложности. Наблюдать за этим просто сказка, особенно когда еще такие спецы рубятся друг против друга.
Но сейчас снова не об этом.
Что я познал недавно?
Оказывается с развитием мощностей компьютеров и, как следствие, эмуляторов старых платформ, появилось такое движение как TAS — Tool assisted-speedrun (инструментальное скоростное прохождение). В двух словах — это показательное прохождение игры до победного конца за рекордное время.
Как это делается. Берется старая “классическая” игра. Запускается на эмуляторе. Работа на эмуляторе позволяет делать с игрой все что угодно — замедлять время, дизассемблировать исходный код, делать временные сохранения достигнутых результатов даже если сама игра такого не позволяет, анализировать недостатки и внутренние особенности “движка” игры, пробовать разные маршруты и т.д. Море всяких трюков. Очевидно, что игра живого игрока во многом ориентирована на безопасность, нежели на время прохождения. “Автоматизированный” же игрок может делать то, что живой никогда делать не будет — может подойти к врагу очень близко, так как заведомо знает, что из-за особенностей движка игры при расстоянии, например, в 2 пикселя, враг тебя никогда не убьет. А живой игрой может даже и 20 пикселей не рискнет, так как ему “на глаз” так проще. И “автоматизированный” игрок может уже не бежать за здоровьем, имея только 1% жизни, так как точно известно, что ему этого хватит до конца уровня.
В общем, шаг за шагом, уровень за уровнем создается и записывается в виде действий “игрока” оптимальный сценарий прохождения игры. После этого найденный оптимальный сценарий подсовывается оригинальной игре (или оригинальной игровой приставке) — либо через механизм проигрывания демо-сессий (если игра поддерживает таковой), либо изготавливается для игры специальный игровой контроллер (клавиатура, мышь или джойстик), который уже управляется не человеком, а компьютером по найденному оптимальному сценарию. И игра проходится, проходится вся и очень быстро. Так например, был пройден классический “Супер Марио” на Ниндендо чуть ли не за 5 минут.
Например, вот один из так называемых speedrun’ов в Дум 2:
и Супер Марио:
Понятно, что такая “игра” далека от получения удовольствия от реальной игры, но это просто другой, но также очень увлекательный жанр.
]]>А все было бы заметно проще, если бы в процессе любой длинной и сложной работы делались бы промежуточные коммиты в систему контроля версий — своеобразные реперные точки, по которым можно пошагово отследить изменения.
Когда же используется централизованная система контроля версий (SCM) многие люди не коммитят незаконченный код, ибо в подавляющем числе случаев работа ведется в ветке, которой пользуется еще кто-то. Закоммитишь сломанный код — услышишь слова радости в свой адрес из другого конца комнаты.
Создание же ветки (нужно, например, для отслеживания изменений при отладки конкретного бага) в централизованной SCM более менее событие. Многие конторы имеют свои правила и процедуры создания веток (именование, причины создания, порядок их удаления и т.д.). Все это можно понять, так как внесения изменений в любой ресурс общего пользования (коим являтся репозиторий централизованной SCM) должны подчиняться каким-то правилам, а иначе будет хаос, и никто не сможет работать.
Что делать если у вас используется централизованная SCM? Просто начните использовать любую из современных распределенных систем параллельно с основной централизованной. Для начала можно вообще не вдаваться в детали хитрой интеграции локальной распределенной SCM и централизованной для автоматизированного переноса коммитов туда-сюда (например, как p4-git для Git и Perforce), а делать все просто: просто коммитить процесс работы в вашу собственную локальную распределенную систему для удобства отслеживания микро изменений, а когда все готово — делать большой коммит на сервер.
Мне приходится работать параллельно с разными SCM, и они преимущественно централизованные (SVN, Perforce, ClearCase), и преимущественно правила коммитов и слияний между ветками очень жесткие и детально прописанные. А про создание собственных веток я уж и не говорю. Но это не мешает мне локально использовать git, в котором в дополнение к официальным веткам сидит десяток моих собственных, коммиты и слияние в которых я делаю десятки раз в день.
Я стараюсь коммитить как можно чаще. Например, добавил новый target в Makefile — коммит, добил новый тест (пусть даже он пока не компилируется толком) — коммит, заставил тест компилироваться — точно коммит, ну а заставил тест работать — стопудово коммит. Решил попробовать новый метод линковки проекта и для этого подкорячить Makefile — создал новую ветку, поигрался, слил результаты с основной веткой и удалил временную. Конец рабочего дня и пора лететь на купание дочки — коммит, даже если исходники представляют собой поле боя, так как завтра тебя с утра могут неожиданно перебросить на Умань чинить срочный баг, и потом уже точно не вспомнить, что там к чему.
Также желательно, чтобы коммиты были логически изолированы. Например, в запале ты исправил сетевую подсистему и добавил кнопку в UI — не стоит объединять все это в один коммит, так как может случиться, что вы заходите эту новую кнопку в параллельной версии, и если это отдельный коммит, то перенести его можно будет простым слиянием или cherry-pick‘ом. Наличие staging area (индекса) в git позволяет легко коммитить выборочно (причем даже файл по кускам). Для Mercurial я нашел более менее похожую возможность в TortoiseHG, когда при коммите можно отметить файлы, которые в него включаются.
А так как каждый коммит требует словесного описания, то волей неволей это заставляет тебя оглядывать в целом, что ты тут понаписал. Для экстренных коммитов в конце дня, когда все может быть тотально сломано, а коммитить надо, то я обычно ставлю префикс “UNFINISHED:” в описание, по которому с утра сразу видно, что в исходниках может быть засада.
Лирическое отступление. С некоторого времени у меня даже всякие самопальные скрипты в UNIXе (а у кого их нет?) и конфигурационные файлы типа .profile
, .Xdefaults
или .vimrc
живут под контролем git’а. Другой пример: скачал я новый gdb-7.0. Развернул, скомпилил. При работе он начал иногда падать на определенных машинах с ошибкой. Интернет сказал, что это известный баг и есть патч. Так вот: сначала сразу после разворачивания оригинального архива дерево исходников gdb помещается в git (git init
&&
git add *
&&
git commit -m "Original gdb-7.0
), а только затем делается патч и тоже коммитится в git. Для чего? Чтобы понимать, что изменено, когда и почему.
Еще одно лирическое отступление. Ни что так помогает понять, насколько “нужен” тебе некий домашний хлам, как его датирование. Записал DVD с бэкапом — кроме названия диска еще надо надписать дату записи. Собрал документы по сданному проекту в архивую папку — поставил дату. Потом, через N лет, этот стикер с датой однозначно решит судьбу предмета и, возможно, определит его в помойку, освободив место в шкафу. В компьютере все это далается еще проще. Ну а история изменений/версий только приятно автоматизируют процесс.
Культура повсеместного использования контроля версий крайне позитивна. А распределенные системы (типа Git, Mercurial или Bazaar) позволяют приобщиться к прекрасному даже если все вокруг вас не хотят (пока!) принять эту культуру.
Посты по теме:
]]>Windows 7 для iTunes (увы, iPhone иначе толком не синхронизируешь), MS Office (в основном только ради Visio) и Visual Studio разнообразных версий. Все остальное происходит в виртуальных машинах. Благодаря аппаратной виртуализации в современных процессорах производительнось практически такая же (и у кого сейчас меньше двух ядер?). А умелое распределение ресурсов между виртуальными машиными вообще позволяет нивелировать потери.
Большинство современных виртуализаторов умеют очень ловко интегрировать десктоп гостевой системы в основную. Например, у меня окно той же виртуализированной Ubuntu выглядит обычным окном Windows, размер которого можно спокойно менять, при этом виртуальная среда автоматически подстраивает свой десктоп на лету под размер этого окна. Раньше, когда аппаратная поддержка виртуализации не была так распространена, и поэтому виртуальные машины реально тормозили, я долгое время использовал Cooperative Linux, так как он виртуализирует только память, и поэтому работает на полной скорости компьютера. Но coLinux не поддерживает многопроцессорность и требует особого ядра Линукса. С самим ядром уже не поиграешься.
В чем прелесть такого подхода с виртуальными машинами? Для начала — это изолированность сред. Не надо держать на одной системе много конфликтующего хлама, ибо постоянно для разных проектов нужны разные компиляторы, разные библиотеки, может и разные операционные системы. Диски/накопители для виртуальных сред — это просто файлы. Их просто копировать, уменьшать/расширять, сохранять, использовать для разных сред и т.д. И миграция с одного физического компьютера на другой упрощается в разы, так как надо просто слить образы машин.
Захотел подключить новый диск в систему или подменить развернутый Oracle на другую версию — пять минут работы. Захотел вернуть назад — еще две минуты. Да и клиенту дать образ настроенной виртуальной машины проще, чем объяснять, как все настраивать с нуля. А он потом еще все и сломает шаловливыми ручками.
Пока я вижу только один значительный минус при домашнем использовании — дисковое пространство, ибо для каждой системы счет сразу идет на гигабайты, а может и их десятки. 4-5 виртуальных машин, и начинаешь думать, куда бы сбросить временно неиспользуемые.
Но и тут есть выход. Например, у меня юниксовый раздел для домашнего (home) каталога используется в нескольких виртуальных машинах. Раздел с проектами тоже может быть замонтирован в разные машины. Повторюсь — управление дисковыми разделами в виде файлов крайне просто и удобно.
Для серверных решений весьма популярна система виртуальных операционных систем ESXi. Небольшой супервизор грузится в начале, и затем дает возможность управлять виртуализацией гостевых систем (диски, память, процессорное время и т.д.). То есть мой Windows 7 является простецким аналогом такого супервизора.
Подходим к сути. Так сейчас уже стало модно заменять стандартный BIOS на что-нибудь нужное, например Линукс с минимальным набором программ для интернета. Проекты coreboot и Splashtop уже давно существуют. Некоторое время назад лично я был очень впечатлен одним их видео:
Выходит, что логичным продолжением было бы зашить вместо BIOSа супервизор виртуальных машин.
Включаешь компьютер, мгновенно стартует супервизор, который дает тебе возможность управлять витуальными средами. Вот получится все одном флаконе на одном компьютере: и Windows, и Mac, и Linux и все на свете.
Проблемы конечно будут — надо делать драйвера виртуальных устройств под поддерживаемые операционные системы, но многое уже имеется в тех же VMware и VirtualBox.
В общем, в дополнение к модной “облачной” модели домашних компьютеров виртуальная модель однозначно имеет будущее. Для тех, кто любит “по-горячее”.
]]>blockquote
, pre
, списков и т.д., чтобы не появлялись ненужные отступы. В результате выходной html становится практически нечитаемым.
Удобство же wiki-разметки в том, что исходник поста выглядит красиво и пригоден для последующего редактирования.
Но в таком подходе цикл работы над постом несколько длинноват: редактор, push через Mercurial на Google Code, просмотр как выглядит на Wiki, затем прогон через скрипт, и если все хорошо, то “cut-paste” в онлайн-редактор Blogspot и финальный отсмотр там. А если обнаруживаются шероховатости, что цикл повторяется сначала. И еще меня раздражал сам скрипт — корявый и непонятный.
Хотелось чего-нибудь легкого и более менее WYSIWYG.
Порыскав в сети на наткнулся на wiki2html. И это оказалось то, что нужно. Я немного подкорячил это под свой формат wiki-разметки и, в итоге получилось это: онлайновый редактор с препросмотром для подготовки постов в Blogspot.
Теперь цикл такой: набиваешь пост в этом редакторе, а он автоматически отображает preview по мере набора вместе с html’ем. Затем cut-paste
html-я в Blogspot, и с большой вероятностью форматирование должно выглядеть как задумано.
Ни разу не претендую на возможные странности моего диалекта Wiki. Желающие могут изменить под себя, ибо форматировщик крайне простой.
Редактор состоит из одного файла wiki2html.html. Его можно просто сохранить локально и поиграться.
Под занавес привожу поддерживаемые команды wiki-разметки (таблиц пока нет).
Ссылки и картинки
[http://example.com/test] — ссылка: http://example.com/test
[http://example.com/test ссылка с текстом] — ссылка с текстом
[http://github.com http://github.com/images/icons/public.png] — картинка со ссылкой
[http://github.com/images/icons/public.png] — картинка
[http://github.com/images/icons/public.png картинка по ссылке] — картинка по ссылке
Текст
'''bold''' — bold
''italic'' — italic
@@[http://google.com]@@ — экранирование любой wiki-разметки
моно`ширин`ный текст — моноширинный текст
Заголовки
= Заголовок1 =
== Заголовок2 ==
=== Заголовок3 ===
==== Заголовок4 ====
===== Заголовок5 =====
Цитирование
Цитата начинается с двух пробелов.
Разделитель
---
Ненумерованный список
Код:
* это
** ненумерованный
* список
Нумерованный список
Код:
# это
## нумерованный
# список
Исходники
Код:
#include <io.h>
int main() {
int a = 1;
return a;
}
Видео
[https://www.youtube.com/watch?v=FrufJFBSoQY]
Посты по теме:
]]>и в целом конкретно этот тренинг посвящен темам из нее.
Очень прикольный дядька. Нескучно, разбавлено подколами аудитории и шутками типа что ребята из Boost’a не иначе как курят шаблоны и т.д. Мне очень понравилось.
Стив сказал, что С++ - это практически все, что делал и делает в жизни. Писал компиляторы, утилиты, разбирался в стандартах, а сейчас вот дает тренинги.
Не могу сказать, что я узнал что-то особенно новое — это было бы странно, так как его книжку (см. выше) читал от корки до корки и периодически к ней возвращаюсь. Хотя, пожалуй, одна мысль меня зацепила: правильное написание конструктора копирования или оператора присваивания для класса, в иерархии которого есть виртуальный базовый класс с данными является весьма запутанной задачей. Тут однозначно нарушается принцип логической независимости уровней иерархии наследования, так как надо точно знать, от каких классов ты унаследован и как их правильно инициализировать при множественном наследовании.
Рекомендация такая: сначала задаешь себе вопрос: а нужно ли мне тут множественное наследование? а нужно ли мне виртуальное множественное наследование?? а нужно ли мне виртуальное множественное наследование с данными в базовом виртуальном классе??? И даже после долгого раздумья лучше сказать “нет”. Лично я не имею ничего против множественного наследования. Но мне не очень нравится как это сделано в С++. И мне не очень нравится как это сделано в Java. Мне очень нравится, как сделано в Go. А именно: в Go полностью разнесены понятия структур данных и интерфейсов. Структуры данных не могут быть унаследованы. Они могут только реализовывать интерфейсы. А наследовать можно только интерфейсы. Поэтому в принципе нельзя при наследовании подцепить чужие данные, а только методы. А нет данных, не и проблемы их инициализации.
Итак, могу просто собрать общие рекомендации от Стива:
if
и case
std::vector
гарантирует линейное размещение элементов, то можно смешивать “старый” код, работающий с указателями, с использованием контейнеров)static_cast
, const_cast
и т.д.), так как они длинные, их нудно набивать и они уродуют вид программы - в общем, все, что нужно для минимизации их наличияPeter Seibel, “Coders at Work”
Книга состоит из интервью с десятком известных программистов. Тут есть создатели UNIX, Netscape, JavaScript, Smalltalk, Haskell, Erland, Ghostscript, ЖЖ и также есть просто Дональд Кнут. В общем, не самые последние люди.
Автор задает всем похожие вопросы: когда и почему вы начали программировать, как вы обычно работаете, где и над чем вы работали и работаете сейчас, что вы думаете о развитии языков программирования за последние десятилетия, что вы можете посоветовать молодым и т.д. Около мемуарная тема опасна тем, что можно элементарно скатиться в старческое брюзжание типа “а вот в наши годы…” или “вы в машинных кодах пишите, и будет вам наука…”, но у всех оказался очень взвешенный взгляд на действительность. Конечно, есть радикальное разделение на функциональщиков и императистов, но уже вопрос религии, нежели возраста.
Многие ссылаются на разные книги - я значительно пополнил свой список на “прочитать”.
Забавно, что почти никто не отозвался однозначно положительно о С++. Либо было однозначное нет типа “очень перегружено, сложно и т.д.”, либо “ну раз уж более ничего пока лучше нет для создания native кода промышленной сложности, то пусть будет”.
Лирическое отступление. Я тут покуриваю Go, и чем дальше, тем больше меня прет. Могу сказать, что я почти для всех своих плюсовых привычек нашел альтернативу в Go. Ну а его врожденная мультипотоковость и ультра быстрая компиляция довершают все.
Также интересно мнение на тему обязательно ли для всем уважающим себя программистам прочитать “Искусство программирования” Кнута или хотя бы иметь в своей библиотеке. Многие признали, что не читали от корки до корки, но как справочник используют.
Как всегда убедился, что я даже и не слышал о некоторых крайне известных вещах. Например, Literate programming от Дональда Кнута или фильтр Блума.
В общем, я нашел книгу очень интересной. Если вас накрывает тоска типа “а можно или всю карьеру программировать?…” или “надо таки подаваться в менеджмент или в архитекторы, а то тут молодые наседают…”. Тут дается отличный, но скрытый ответ: любой из этих путей может принести и удовлетворение и, что важно, достаток. В этом прелесть нашей профессии. Просто делай то, от чего хочется работать до ночи, смотри по сторонам, интересуйся, что происходит вокруг, и не переживай, что можешь оказаться ненужным - это не так.
]]>Какой логичный выход? Использовать для записей компьютер.
Долгое время я использовал просто текстовый файл, например в Ворде. Например, начался проект или новая работа — сразу валит поток фактов, которые я в этот файл записываю. Обычно где-то раз в неделю я вбиваю записи из блокнота и тонны бумажек.
Так как информации много, то ее хочется как-то логично организовывать - главы, разделы, подразделы и т.д. Но со временем я заметил, что основной способ навигации впоследствии - это ни разу не оглавление, а просто поиск по словам. Например, мне нужно вспомнить, что мне там объясняли на тему как вбивать расходы или где смотреть отчеты по ночным сборкам. Я просто нажимаю CTRL-F
и ввожу слово “расходы” или “ночные”. Как говориться, “Don’t sort. Search!”
Наличие всего в одном файле упрощает техническую сторону вопроса - хочешь в Ворде работай, хочешь в Vi. И бэкапить удобно или в контроль версий засунуть.
Кстати, интересное наблюдение. В случае новой работы по первости обычно приходится фиксировать много рутинных вещей, типа как запускать сборку, как организованы исходники, где находится то или это и т.д. Обычно месяца через два-три поток стихает. Но когда приходит новый человек, и я ему выдаю такой файл, который в свое время создавал сам - обычно люди офигевают от того, получают ответы на почти все вопросы новичка, и говорят спасибо.
Итак, простой текстовый файл как всегда на коне. Но некоторое время назад, я перешел на TiddlyWiki. Я давно знал об этой штуковине, но как-то все не решался полностью на нее перейти. Сомневался в надежности, и способности нормально работать с большим объемом и т.д. Но недавно я перешел на новую работу, все завертелось снова и, я полностью таки начал записи вести в TiddlyWiki.
Что это такое? Поясню кратко, для тех кто не слышал. TiddlyWiki - это Wiki движок, который представляет собой один единственный html-файл. Как это работает: вы просто открываете этот файл в браузере, и движок на JavaScript позволяет создавать записи, редактировать их, ссылать записи друг на друга, удалять и, что важно, искать Но еще этот движок умеет делать самое интересное - сохранять все, что вы ввели в этот же файл. То есть когда вы откроете этот html-файл завтра, в нем будет все, что вы ввели раньше.
Я обычно работаю так: есть закладка в браузере, которая открывает локальный файл с TiddlyWiki. А так как браузера практически всегда открыт - запуск занимает мгновения. Так как этот Wiki, то доступны все удобства Wiki-разметки и взаимных ссылок (кстати, сам сайт www.tiddlywiki.com построен на TiddlyWiki). Создание новой заметки (tiddler’а) происходит мгновенно. Для навигации можно сделать удобное меню слева, но просто пользуюсь поиском. Когда данных много - это единственный способ что-то найти.
Что лично мне нравится TiddlyWiki-подходе в целом:
Нужен только браузер с установленым плагином Java и малюсенький файл Saver.jar
, который поставляется вместе с TiddlyWiki, и который надо просто положить в тот же каталог. Поэтому с wiki-файлом можно работать хоть в Windows, хоть в UNIX, хоть на Маке. Я лично проверял в Chrome, Firefox и IE).
Файл можно выложить на web-сервер, например в интранете, и таким образом публиковать записи. Конечно, для просматривающих его через web файл будет только для чтения.
Так как файл по сути своей текстовый (обычный html), то в случае чего, можно выдрать из него данные простым текстовым редактором, хотя у меня такой необходимости еще не было.
Движок TiddlyWiki при каждом самосохранении умеет делать копию текущего состояния. Я эти файлы обычно архивирую раз в неделю, и solid-архивация в один архив позволяет хранить всю историю с минимальным увеличением архива при каждом новом сохранении. И всегда можно вернуться к определенной старой версии.
И главное: удобный поиск!
Небольшой трюк для пользователей Хрома. Для нормальной работы TiddlyWiki нужно, чтобы были включены cookie. По умолчанию в Хроме при работе с локальными файлами cookie выключены, поэтому Хром следует запускать с ключом --enable-file-cookies
.
Не спрашивайте второй раз одно и то же. Лучше запишите ответ в первый раз, а спросите что-нибудь новое.
]]>Удивительное дело, что еще остались люди, которые сомневаются, что open source - это единственная модель, с которой можно выживать при современном объеме и сложности софта.
Но вернемся к теме.
Chromium OS - загрузка за 4 секунды до получения браузера. Ну а параллельно можно грузить винды в виртуальную машину.
]]>Как следствие того, что каждый коммит готовится, отлаживается и проверяется ощутимое время (благо это только bug fix’ы, размер которых обычно невелик), и даже формальная сторона вопроса может занять пару дней, и очень часто случается, что когда дело доходит непосредственно до команды “commit”, все оканчивается конфликтом, так как кто-то уже успел потрогать твой кусок кода и залить это на сервер. И надо ручками сливать обновленную версию со своими изменениями. А если файл не один, то начинается головная боль.
Так как я только недавно в этой теме, то после второго же коммита решил упростить себе жизнь в плане слияния при конфликте.
Расчехлил я git, и теперь все выглядит так: каждый мой багфикс живет в отдельном репозитории git (фактически, каталог) с двумя ветками. В одной я независимо работаю над исправлениями, веду git’ом историю этой работы, а во вторую ветку периодически синхронизирую состояние исходников из главного репозитория и сливаю с ними свою ветку одной единственной командой “git merge”.
В плане распределенных SCM я сейчас в основном работаю с mercurial, так как Google Code его поддерживает. Но все таки git - это невероятно мощный инструмент (правда когда в рабочем процессе не фигурирует Windows, ибо виндовая версия git’а портирована крайне криво).
По началу многое запутывает. Меня по первости крайне сбивала с толку идея staging area (или его еще называют - index). Эдакое промежуточное звено между локальными изменениями и самим репозиторием. Получается, что git diff может показывать три разные вещи: разницу между локальными изменениями и индексом (а не репозиторием - это будет по умолчанию, что обычно и вводит новичков в ступор), разницу между индексом и репозиторием и наконец разницу между локальными изменениями и репозиторием. Индекс (или staging area) позволяет при коммите выбирать, что именно из локальных файлов надо закоммитить, а не все подряд. В коммите участвуют только файлы, находящиеся в индексе. Причем самое интересное, что можно включать в индекс куски измененных файлов (например, я добавил в файл два новых класса, но закоммитить могу только выборочно один из них).
Вам уже нравится?
Или например, откат всех локальных изменений (очень частая операция) может быть выполнена как минимум двумя способами (через git checkout и через git reset), или откат уже сделанного коммита можно также провернуть минимум двумя путями (git reset или git revert) в зависимости от того, хотите ли вы видеть потом этот откат в истории.
Обилие функций и их некоторая непохожесть на общепринятые стандарты команд SCM немного обескураживают сначала. Но немного въехав в тему начинаешь ощущать всю мощь. Например, наличие staging area и git stash (когда можно временно заморозить текущее состояние, сделать какую-то быструю минутную работу и вернуться к основной теме) - весьма уникальные возможности git’a.
В плане GUI - gitk дает все необходимое.
Единственное, что надо выбрать по вкусу самостоятельно - это программу для проведения слияния при разрешении конфликта в графическом режиме. Тут у всех свои предпочтения.
Использование правильных и удобных инструментов сильно ускоряет работу. И время, потраченное в начале на выбор и настройку правильных служебных программ однозначно окупится в будущем.
Надеюсь, мне удалось привлечь в ряды пользователей git еще пару тройку энтузиастов.
Однажды привыкнув с постоянному наличию под рукой контроля версий, хочется, чтобы она была везде. Даже при починке автомобиля.
Посты по теме:
]]>Выдам все желающим в порядке поступления запросов. Естественно, имеет смысл только при наличии ящика на GMail’е, поэтому просьба указывать гугловский адрес, куда высылать приглашения.
Как показывает практика, реально после высылки приглашение приходит не сразу (может занять пару дней), так что надо потерпеть.
И еще – делитесь сами приглашениями, если они у вас есть или будут. Wave хорош, когда там есть люди.
]]>Из лично опыта могу сказать, что весьма часто вторая пара незамутненных многочасовым смотрением на данный исходник глаз моментально замечает лажу, и в очередной раз удается избежать ночной fuck up, когда начинают валить письма от системы ночного тестирования, что туча QА тестов сломана из-за глупого недосмотра.
Perforce - отличная система для работы с реально большимы объемами репозиториев и кодовой базы в целом, но в ней нет встроенного механизма для code review. Google решили эту проблему сами.
В данном видео небезызвестный Гвидо ван Россум рассказывает о системе Mondrian, построенной на основе Perforce, которая применяется в Google для процесса code review.
Также мельком упоминается идея организации работы с исходниками в Google в общем. Например, что практически каждый инженер работает с огромным общим для все остальных разделом NFS, что позволяет видеть сразу, что происходит в других проектах.
]]>Все как в старые добрые времена, только прямо в браузере (нажмите на картину ниже).
Текущая версия 0.6. Уже помимо самой эмуляции и набора игр есть встроенный ассемблер, на котором можно прямо в окне эмулятора писать и компилировать код для Intel 8080, и почти интерактивный дизассемблер, которым можно просматривать не только код, но и данные.
Несколько картинок.
Собственно, эмулятор (игра Volcano):
Ассемблер:
Дизассемблер:
Также постепенно дополнется список игр.
В плане браузеров я делаю в основном только под Хром, но говорят, что в Firefox и Safari тоже работает с разной степенью мини-глюков.
Удовольствие от этого проекта очень сложно объяснить. Тут что-то глубинное.
]]>class Date { public: Date(int year, int month, int day) { ... } };
К сожалению, не весь мир пользуется логичной нотацией Год/Месяц/День или День/Месяц/Год. Иногда люди пишут Месяц/День/Год. Хотя и первые два легко перепутать. Вот к чему я веду: где-то в далеком от описания класса коде кто-то пишет:
Date d(2009, 4, 5);
Что он этим хотел сказать? 4-е Мая или 5-е Апреля? Сложно быть уверенным, что пользователь такого класса когда-нибудь не перепутает порядок аргументов.
Можно улучшить дизайн? Да.
Например, так:
class Year { public: explicit Year(int year) : year_(year) {} operator int() const { return year_; } private: int year_; };
И аналогично:
class Month { ... }; class Day { ... };
Интерфейс самого класса Date может быть таким:
class Date { public: Date(Year year, Month month, Day day); Date(Month month, Day day, Year year); Date(Day day, Month month, Year year); }
И использовать класс надо так:
Date d(Year(2010), Month(4), Day(5));
или
Date d(Month(4), Day(5), Year(2010));
Результат будет всегда предсказуем и виден в вызывающем коде. Тут все inline’овое, так что эти три “лишние” класса никакого замедления не дадут.
Согласен, писанины немного больше, но зато полная гарантия от опечаток и, как следствие, глупых, но коварных ошибок.
Возражения есть?
]]>Сижу, отлаживаю новый онлайновый ассемблер в своем эмуляторе Радио-86РК. Под отладкой понимается ёрзанье с применением html’я.
Для сборки финального html-файла из кучи мелких у меня написана примитивная программа. Вот ее фрагмент:
while (!feof(f)) { char line[1024]; *line = 0; fgets(line, sizeof(line), f); printf(line); }
Подразумевается, что данный код прострочно копирует данные из файла f
на стандарный вывод.
Даже если отставить в сторону использование буфера с константной длиной и прочих “штучек” языка С, этот код имеет одну проблему, которая стоила мне сомнений в наличии сознания. До каких-то пор все работало отлично, но как только я начал использовать процентные значения для широт и высот в html, начались странности.
Получалось, что вместо, например:
<table width="100%">
на выходе было:
<table width="100">
Вы, наверное, уже догадались, в чем тут дело. Но, признаюсь, я искал проблему минут тридцать.
Вместо:
printf(line);
надо писать:
printf("%s", line);
А иначе все процентные символы будут расцены как указатели форматов, ибо первый параметр printf()
- это не просто строка, а формат, и в случае их неэкранирования будут уделены, что и происходило в моем случае.
Вывод (который следует после начального “сам дурак”): Лучше писать на С++ и использовать потоки для форматного вывода.
Лирическое отступление. Кстати, онлайновый ассемблер очень огранично вписался в эмулятор. Спасибо Вячеславу Славинскому за оригинальный код этого ассемблера. Особенно меня радует возможность автоматической фоновой компиляции. Теперь можно, прямо не отходя от эмулятора, переключиться в ассемблер, написать что-нибудь на диалекте Intel 8080 (КР580), скомпилировать и загнать прямо в эмулятор.
]]>Использование Mock-объектов является очень интересной темой. И владение ей позволяет перевести unit-тестирование на принципиально иной уровень.
Как рассказано в статье, языки программирования типа Python или Java благодаря встроенному механизму Reflection позволяют строить Mock-объекты почти автоматически. С++ не дает такой роскоши, но гугловцы проделали отличную работу, создав Google Mock. Практически все, что можно как-то упростить или автоматизировать в плане mock-дел в С++, сделано и сделано добротно.
Соглашусь, что поначалу вся эта тема с Mock-объектами выглядит несколько громоздко и сложновато, но тут как с эргономичной клавиатурой - надо сначала привыкнуть, а потом все окупится сполна.
Посты по теме:
]]>Захожу сегодня и что вижу:
Появился новый пункт в закладке Source — Clone (обведено красным). Это то, чего так долго ждали большевики — клонирование проектов.
Если администратор проекта не хочет добавлять вас в контрибуторы, а вы таки хотите показать людям свою лепту в данном проекте, то теперь вы можете сделать клон проекта и продолжить работу над своей веткой независимо. История изменений полностью наследуется от оригинала.
Клон может быть в свою очередь тоже склонирован.
Больше открытых проектов, хороших и разных!
Посты по теме:
]]>В презентации описывается не только сама библиотека, ее возможности и примеры использования, но и сам подход к разработке через тестирование (test driven development), рассмотрены несколько базовых советов по написанию удобного для тестирования кода и т.д.
В целом, не самые плохие десять минут, чтобы поглядеть и немного поразмыслить.
]]>... int x = (i << 8) | (i >> 2) | ((i & 0x06) ^ 0xAA); // if (x >= 0x12345) // x = x >> 3; calc_something_complicated(x); ...
мне хочется рвать и метать. Расчехлить blame, найти автора и заглянуть ему в глаза.
Иначе, что мне остается: только думать, что автор этих строк, видимо, бился с формулой, пытался подогнать результат под что-то (возможно, какой-то тест). Достиг ли он результата? Или может оно так и продолжает глючить… Кто знает. Единственное, о чем этот код однозначно говорит, что автор не был уверен в том, что пишет. Потому, что если он был уверен, то удалил бы этот фрагмент или раскомментировал бы его навсегда.
На втором месте у меня стоит отладочная печать, навеки оставленная в коде:
... int x = (i << 8) | (i >> 2) | ((i & 0x06) ^ 0xAA); // std::cerr << "На этот раз x = " << x << std::endl; calc_something_complicated(x); ...
Снова получается, что автор сомневался и так и не отладил все до конца.
Конечно, подобные куски могут появляться просто по невнимательности и забывчивости, но как может помочь это оправдание? Никак.
Ну на третьем месте нашего хит-парада - блоки TODO.
... int x = (i << 8) | (i >> 2) | ((i & 0x06) ^ 0xAA); // TODO: Заменить тип int на int64, так как на носу эра 64-разрядных машин. // Программист Вася, 06.10.2009. Если что, вы знаете где меня искать. calc_something_complicated(x); ...
Тут еще куда ни шло. Куски TODO можно найти автоматическим поиском при подготовке релиза и перед ответственным слиянием. Но каждый TODO должен быть подписан и датирован, а лучше еще и детально объяснен. Ни что так не помогает оценить “нужность” куска кода, как дата его последней модификации.
Итак, вывод тут только один: в production коде никогда не должно быть закомментированных кусков. А если они есть, то они должны сопровождаться четкими комментариями, поясняющими их суть.
]]>Одна из главных новых возможностей - это event listener API. А попросту говоря, возможность полностью контролировать процесс печати результатов тестирования. Это позволяет формировать отчеты по тестированию в нужном формате без изменения кода библиотеки.
Что новенького?
Например, стандартный вывод при выполнении элементарного теста (файл runner.cpp
):
#include "gtest/gtest.h" TEST(One, Simple) { EXPECT_EQ(1, 2); } int main(int argc, char** argv) { ::testing::InitGoogleTest(&argc, argv); return RUN_ALL_TESTS(); }
будет таким:
[==========] Running 1 test from 1 test case.
[----------] Global test environment set-up.
[----------] 1 test from One
[ RUN ] One.Simple
runner.cpp(4): error: Value of: 2
Expected: 1
[ FAILED ] One.Simple (15 ms)
[----------] 1 test from One (15 ms total)
[----------] Global test environment tear-down
[==========] 1 test from 1 test case ran. (31 ms total)
[ PASSED ] 0 tests.
[ FAILED ] 1 test, listed below:
[ FAILED ] One.Simple
1 FAILED TEST
[==========] Running 1 test from 1 test case.
[----------] Global test environment set-up.
[----------] 1 test from One
[ RUN ] One.Simple
runner.cpp(4): error: Value of: 2
Expected: 1
[ FAILED ] One.Simple (15 ms)
[----------] 1 test from One (15 ms total)
[----------] Global test environment tear-down
[==========] 1 test from 1 test case ran. (31 ms total)
[ PASSED ] 0 tests.
[ FAILED ] 1 test, listed below:
[ FAILED ] One.Simple
1 FAILED TEST
Для задания иного формата вывода нужно реализовать свой event listener
(назовем его сервис печати). Например (файл runner.cpp
):
#include "gtest/gtest.h" using namespace ::testing; // Данный класс заменит стандартный сервис печати. class LaconicPrinter : public ::testing::EmptyTestEventListener { // Вызывается до начала работы теста. virtual void OnTestStart(const TestInfo& test_info) { printf("*** Test %s.%s starting.\n", test_info.test_case_name(), test_info.name()); } // Вызывается при срабатывании какого-либо утверждения или явного вызова // функции SUCCESS(). virtual void OnTestPartResult(const TestPartResult& test_part_result) { printf("%s in %s:%d\n%s\n", test_part_result.failed() ? "*** Failure" : "Success", test_part_result.file_name(), test_part_result.line_number(), test_part_result.summary()); } // Вызывается после выполнения теста. virtual void OnTestEnd(const TestInfo& test_info) { printf("*** Test %s.%s ending.\n", test_info.test_case_name(), test_info.name()); } }; TEST(One, Simple) { EXPECT_EQ(1, 2); } int main(int argc, char** argv) { ::testing::InitGoogleTest(&argc, argv); // Получаем ссылку на список сервисов печати. ::testing::TestEventListeners& listeners = ::testing::UnitTest::GetInstance()->listeners(); // Удаляем стандартный сервис печати. delete listeners.Release(listeners.default_result_printer()); // Добавляем наш сервис в список. Google Test самостоятельно удалит этот объект. listeners.Append(new LaconicPrinter); return RUN_ALL_TESTS(); }
Теперь отчет по работе тестов будет выглядеть так:
*** Test One.Simple starting.
*** Failure in runner.cpp:31
Value of: 2
Expected: 1
*** Test One.Simple ending.
Необходимо отметить, что одновременно может быть зарегистрировано несколько сервисов печати. Но в этом случае их выводы могут смешиваться и превращаться в кашу. Для избежания этого мы принудительно удаляем стандартный сервис печати, чтобы его вывод не мешал нашему.
Полностью интерфейс сервиса печати выглядит так:
class EmptyTestEventListener : public TestEventListener { public: // Вызывается при начале прогона всех тестов. virtual void OnTestProgramStart(const UnitTest& unit_test); // Вызывается при начале очередной итерации тестирования. Google Test // позволяет управлять количеством итерации при прогоне тестов. virtual void OnTestIterationStart(const UnitTest& unit_test, int iteration); // Вызывается до функции Environment::SetUp(), устанавливающей необходимое // окружение для работы всех тестов. virtual void OnEnvironmentsSetUpStart(const UnitTest& unit_test); // Вызывается после функции Environment::SetUp(), устанавливающей необходимое // окружение для работы всех тестов. virtual void OnEnvironmentsSetUpEnd(const UnitTest& unit_test); // Вызывается при начале прогона группы тестов (у которых первый параметр // макроса TEST/TEST_F одинаковый). virtual void OnTestCaseStart(const TestCase& test_case); // Вызывается при начале работы теста. virtual void OnTestStart(const TestInfo& test_info); // Вызывается при срабатывании утверждения в тесте или явного вызова // функции SUCCESS(). virtual void OnTestPartResult(const TestPartResult& test_part_result); // Вызывается после завершения работы теста. virtual void OnTestEnd(const TestInfo& test_info); // Вызывается после прогона группы тестов. virtual void OnTestCaseEnd(const TestCase& test_case); // Вызывается до функции Environment::TearDown(), производящей освобождение // ресурсов, занятых Environment::StartUp(). virtual void OnEnvironmentsTearDownStart(const UnitTest& unit_test); // Вызывается после функции Environment::TearDown(). virtual void OnEnvironmentsTearDownEnd(const UnitTest& unit_test); // Вызывается после очередной итерации тестирования. virtual void OnTestIterationEnd(const UnitTest& unit_test, int iteration); // Вызывается после прогона всех тестов. virtual void OnTestProgramEnd(const UnitTest& unit_test); };
Также из значимого можно отметить новый ключ командной строки ~, позволяющий запускать тесты в случайном порядке. Ключом --gtest_random_seed=SEED
можно “управлять” случайностью этого порядка. Если SEED
равен 0, то случайность будет действительно случайной, так как случайная последовательность будет определяться текущим временем.
Что приятно, теперь формат XML файлов, генерируемых при использовании ключа --gtest_output
, полностью совместим с форматом JUnit. Это значит, что, например, система автоматической сборки, тестирования и интеграции Hudson теперь понимает отчеты Google Test без дополнительный конвертации.
Также теперь при работе в Visual Studio сообщения о сбойных тестах выводятся прямо в окно “Output”, что позволяет, кликая на них, сразу находить строки, где сбоят тесты. Здорово, что данная фича основана на моем коде.
Еще, теперь время работы тестов печатается всегда, по умолчанию, то есть опция --gtest_print_time
будто бы всегда включена.
Есть еще несколько незначительных улучшений:
tuple
для независимости от boost
при использовании Combine()
Радость, одно слово.
Я перестал что-то писать что-либо на C++ без тестов, а Google Test делает этот процесс простым и легким.
Я уже обновился до версии 1.4.0, а вы?
Ссылки по теме:
]]>const T*
при объявления указателя полностью эквивалентно записи T const*
, ибо тут важно только, что const
стоит до знака *
, а порядок его употребления с именем типа Т
роли не играет:
const T* p;
и
T const* p;
объявляют указатель на константный объект, а не константный указатель, то есть значение самого указателя можно менять:
T const* p; ... p = NULL;
Но менять сам объект нельзя:
T const* p; ... p->some_member = 0; // ОШИБКА: error C2166: l-value specifies const object
Но все это была вводная, и сейчас не об этом.
Меня больше интересуют читабельность исходников. Я могу ошибаться, но как мне кажется, что с общечеловеческой точки зрения употребление const
в начале выражения (например, const T* p;
) подразумевает константность всего выражения, и, собственно, не важно, что там на самом деле указатель, и по правилам С++ данный const
значит только константность объекта, а не указателя.
Поэтому запись T const* p;
может читаться несколько иначе, а именно: “тип T, который константный, и на него объявляется указатель”. Читабельность немного лучше.
Конечно, все это вопросы стиля во многом, да и всегда следует в первую очередь соблюдать стиль, уже принятый существующем исходнике, но есть вы только учитесь, или начинаете новый проект и что-то еще, когда можно попробовать что-то новое в стиле, как мне кажется, не стоит отказывать себе в этом опыте.
]]>Поэтому я до сих пор питаю слабость к данному раритету. К результат этой слабости я постоянно писал эмуляторы этого компьютера.
Первый был под ДОС. Оригинальный сайт сего творения я храню до сих пор в неизменном виде. Этот эмулятор был весьма неплох: там был и встроенный отладчик, и метода взлома игр и т.д. Но ДОС ушел, поэтому данный эмулятор работает теперь разве что в DosBox‘e. Исходники творения можно скачать.
Вторая реинкарнация любимого РК была уже под Windows и работала через SDL. Тут уже не было встроенного отладчика, да и проект так и остался сырым (хотя и работающим), поэтому на публике лежит только бинарь с комплектом игр.
И вот, пару дней назад я наткнулся вот на эмулятор Спектрума на чистом JavaScript’е! - ни апплетов, ни activex’ов.
Так я от этой темы завелся, что за день-два оживил старого монстра РК на платформе JavaScript. Оказывается правильные браузеры дают весьма недурственную скорость скриптования. 2d графика реализуется через тэг canvas из HTML5.
Получился проект - Эмулятор Радио-86РК на JavaScript.
Эмулятор и набор игр живут в одном единственном файле. Можно нажать на ссылку, и эмулятор запустится в браузере. Внизу есть селектор для выбора игры, и возможность поиграться со размерами экрана и скоростью.
Эмуляция происходит на уровне команд процессора Intel 8080.
Скриншот классической игры Volcano, сделанный в этом эмуляторе.
На текущий момент я проверял только в Google Chrome 4.*. Думаю, я не буду заморачиваться особо на тему совместимости с другими браузерами, хотя посмотрим, как пойдет. В IE точно работать не будет, а вот про FF и Оперу ничего сказать не могу, пока.
Волшебный мир Радио-86РК снова вернулся!
P.S. К слову сказать, лучший эмулятор РК (и множества совместимых моделей), что я видел - это эмулятор Виктора Пыхонина. Может и он мигрирует на модную платформу со временем.
Update: Обновил эмулятор до версии 0.2. Изменения незначительные: добавил некоторые системные программы (языки программирования, отладчики, редакторы и т.д.) и немного улучшил селектор выбора игры, который теперь срабатывает при нажатии кнопки “Run”.
Update: Обновил эмулятор до версии 0.3. Отрисовка экране теперь работает значительно быстрее и не так грузит процессор.
Update: В версии 0.4 теперь встроенный ассемблер. Можно писать и ассемблировать прямо в окне эмулятора. После есть возможно загрузить результат в сам эмулятор и запустить командой “G” в Мониторе.
Скриншот этого ассемблера:
Update: В версии 0.6 теперь есть встроенный дизассемблер. Им можно просматривать не только программый код, но и данные.
Скриншот дизассемблера:
]]>vs_double_semicolumn.c
):
void main() { int a;; int b; }
Компилируем (в режиме языка С
, то есть без /TP
):
cl vs_double_semicolumn.c
Результат:
vs_double_semicolumn.c
vs_double_semicolumn.c(3) : error C2143: syntax error : missing ';' before 'type'
Результат в Codegear/Borland примерно такой же (хотя описание ошибки более ясное):
CodeGear C++ 5.93 for Win32 Copyright (c) 1993, 2007 CodeGear
vs_double_semicolumn.c:
Error E2140 vs_double_semicolumn.c 3: Declaration is not allowed here in function main
*** 1 errors in Compile ***
Проблемка заключается в случайной опечатке в виде двойного символа ;
. Кстати, пример абсолютно реальный, из жизни. Случайная опечатка - и сразу много вопросов.
Получается, что второй символ ;
тут трактуется как пустой оператор, а не пустая декларация переменной. Компилятор решает, что объявления переменных закончены, и начался блок операторов, поэтому резонно ругается на попытку объявить переменную b
там, где уже должны быть операторы.
Проверил на gcc
, на родных компиляторах AIX, Solaris и HP-UX. Эти все съели пример без проблем.
bcc32_5.93_cast_bug.cpp
):
class A {}; class C {}; A* a; A* b = static_cast<C*>(a);
Компилируем в bcc32.exe (версия 5.93) из Codegear RAD Studion 2007:
bcc32 -c bcc32_5.93_cast_bug.cpp
Падает c internal compiler error:
CodeGear C++ 5.93 for Win32 Copyright (c) 1993, 2007 CodeGear
bcc32_5.93_cast_bug.cpp:
Fatal F1004 bcc32_5.93_cast_bug.cpp 4: Internal compiler error at 0x44b34e with base 0x400000
Fatal F1004 bcc32_5.93_cast_bug.cpp 4: Internal compiler error
Люблю собирать падения компиляторов на стадии компиляции. А у вас есть что-нибудь подобное в загашнике?
]]>virtual_funct_const.cpp
):
#include <iostream> class A { public: A() { construct(); } ~A() { destruct(); } virtual void construct() { std::cout << "A::construct()" << std::endl; } virtual void destruct() { std::cout << "A::destruct()" << std::endl; } }; class B: public A { public: B() { construct(); } ~B() { destruct(); } virtual void construct() { std::cout << "B::construct()" << std::endl; } virtual void destruct() { std::cout << "B::destruct()" << std::endl; } }; int main() { B b; return 0; }
Что напечатает эта программа?
А вот что:
A::construct()
B::construct()
B::destruct()
A::destruct()
Получается, что конструкторы и деструкторы классов A
и B
при вызове объявленных виртуальными функций construct()
и destruct()
реально вызывали функции только своего класса.
В этом нет никакого секрета, а просто есть правило: виртуальная функция не является виртуальной, если вызывается из конструктора или деструктора.
Правило надо заучивать, что неудобно. Проще понять принцип. А принцип тут в краеугольном камне реализации наследования в C++: при создании объекта конструкторы в иерархии вызываются от базового класса к самому последнему унаследованному. Для деструкторов все наоборот.
Что получается: конструктор класса всегда работает в предположении, что его дочерние классы еще не созданы, поэтому он не имеет права вызывать функции, определенные в них. И для виртуальной функций ему ничего не остается, как только вызвать то, что определено в нем самом. Получается, что механизм виртуальных функций тут как-бы не работает. А он тут действительно не работает, так как таблица виртуальных функций дочернего класса еще не перекрыла текущую таблицу.
В деструкторе все наоборот. Деструктор знает, что во время его вызова все дочерние классы уже разрушены и вызывать у них ничего уже нельзя, поэтому он замещает адрес таблицы виртуальных функций на адрес своей собственной таблицы и благополучно вызывает версию виртуальной функции, определенной в нем самом.
Итак, виртуальная функция не является виртуальной, если вызывается из конструктора или деструктора.
]]>Программ для данной задачи существует превеликое множество. Но лично я очень давно использую для этих целей свой собственный велосипед. Причин тут несколько. Основная - мне нужна одна программа, одинаково работающая на многих платформах, включая даже Windows. Вторая по значимости причина - возможность налету что-то подкручивать, допиливать, вставлять миникуски кода для анализа конкретного протокола и т.д. Получается, что скриптовой язык тут является хорошим подспорьем.
Несколько лет назад первые версии моей утилиты были на PHP, но текущая версия переписана на Питоне.
Исходник небольшой, а, как мне кажется, разглядывание исходников должно радовать большинство программистов, особенно, если есть что покритиковать, поэтому приведу его прямо здесь (см. ниже).
Ни разу не претендую на оптимальность или крутизну использования Питона, поэтому принимаю любую критику.
Основные особенности и возможности:
Пример использования.
Запускаем сервер:
python pyspy.py -a 10.44.5.138 -p 5467 -l 9999 -L trace.log
Запускаем клиента:
telnet localhost 9999
и вводим GET / HTTP/1.0<ENTER><ENTER>
.
В файле лога и в консоли получаем вот такое:
0000: Listen at port 9999, remote host ('10.44.5.138', 5467)
0000: Connection accepted from ('127.0.0.1', 15223), thread 1 launched
0001: Thread started
0001: Connecting to ('10.44.5.138', 5467)...
0001: Remote host: ('127.0.0.1', 15223)
0001: Recevied from ('127.0.0.1', 15223) (1)
0001: ----: 00-01-02-03-04-05-06-07-08-09-0A-0B-0C-0D-0E-0F
0001: ------------------------------------------------
0001: 0000: 47 | G
0001: Sent to ('10.44.5.138', 5467) (1)
0001: Recevied from ('127.0.0.1', 15223) (13)
0001: ----: 00-01-02-03-04-05-06-07-08-09-0A-0B-0C-0D-0E-0F
0001: ------------------------------------------------
0001: 0000: 45 54 20 2F 20 48 54 54 50 2F 31 2E 30 | ET / HTTP/1.0
0001: Sent to ('10.44.5.138', 5467) (13)
0001: Recevied from ('127.0.0.1', 15223) (2)
0001: ----: 00-01-02-03-04-05-06-07-08-09-0A-0B-0C-0D-0E-0F
0001: ------------------------------------------------
0001: 0000: 0D 0A | ..
0001: Sent to ('10.44.5.138', 5467) (2)
0001: Recevied from ('127.0.0.1', 15223) (2)
0001: ----: 00-01-02-03-04-05-06-07-08-09-0A-0B-0C-0D-0E-0F
0001: ------------------------------------------------
0001: 0000: 0D 0A | ..
0001: Sent to ('10.44.5.138', 5467) (2)
0001: Recevied from ('10.44.5.138', 5467) (379)
0001: ----: 00-01-02-03-04-05-06-07-08-09-0A-0B-0C-0D-0E-0F
0001: ------------------------------------------------
0001: 0000: 48 54 54 50 2F 31 2E 31 20 33 30 32 20 46 6F 75 | HTTP/1.1 302 Fou
0001: 0010: 6E 64 0D 0A 44 61 74 65 3A 20 46 72 69 2C 20 30 | nd..Date: Fri, 0
0001: 0020: 34 20 53 65 70 20 32 30 30 39 20 30 38 3A 35 33 | 4 Sep 2009 08:53
0001: 0030: 3A 30 33 20 47 4D 54 0D 0A 53 65 72 76 65 72 3A | :03 GMT..Server:
0001: 0040: 20 41 70 61 63 68 65 0D 0A 50 72 61 67 6D 61 3A | Apache..Pragma:
0001: 0050: 20 6E 6F 2D 63 61 63 68 65 0D 0A 45 78 70 69 72 | no-cache..Expir
0001: 0060: 65 73 3A 20 46 72 69 2C 20 30 31 20 4A 61 6E 20 | es: Fri, 01 Jan
0001: 0070: 31 39 39 39 20 30 30 3A 30 30 3A 30 30 20 47 4D | 1999 00:00:00 GM
0001: 0080: 54 0D 0A 43 61 63 68 65 2D 63 6F 6E 74 72 6F 6C | T..Cache-control
0001: 0090: 3A 20 6E 6F 2D 63 61 63 68 65 2C 20 6E 6F 2D 63 | : no-cache, no-c
0001: 00A0: 61 63 68 65 3D 22 53 65 74 2D 43 6F 6F 6B 69 65 | ache="Set-Cookie
0001: 00B0: 22 2C 20 70 72 69 76 61 74 65 0D 0A 4C 6F 63 61 | ", private..Loca
...
[обрезано]
...
0001: 0100: 76 3D 31 0D 0A 43 6F 6E 6E 65 63 74 69 6F 6E 3A | v=1..Connection:
0001: 0110: 20 63 6C 6F 73 65 0D 0A 43 6F 6E 74 65 6E 74 2D | close..Content-
0001: 0120: 54 79 70 65 3A 20 74 65 78 74 2F 68 74 6D 6C 0D | Type: text/html.
0001: 0130: 0A 0D 0A 52 65 64 69 72 65 63 74 20 70 61 67 65 | ...Redirect page
0001: 0140: 3C 62 72 3E 3C 62 72 3E 0A 54 68 65 72 65 20 69 | <br><br>.There i
0001: 0150: 73 20 6E 6F 74 68 69 6E 67 20 74 6F 20 73 65 65 | s nothing to see
0001: 0160: 20 68 65 72 65 2C 20 70 6C 65 61 73 65 20 6D 6F | here, please mo
0001: 0170: 76 65 20 61 6C 6F 6E 67 2E 2E 2E | ve along...
0001: Sent to ('127.0.0.1', 15223) (379)
0001: Connection reset by ('10.44.5.138', 5467)
0001: Connection closed
Теперь, собственно, исходник:
#!/usr/bin/python import socket, string, threading, os, select, sys, time, getopt from sys import argv def usage(): name = os.path.basename(argv[0]) print "usage:", name, "-l listen_port -a host -p port [-L file] [-c] [-h?]" print " -a host - address/host to connect" print " -p port - remote port to connect" print " -l listen_port - local port to listen" print " -L file - log file" print " -c - supress console output" print " -h or -? - this help" print " -v - version" sys.exit(1) PORT = False REMOTE_HOST = REMOTE_PORT = False CONSOLE = True LOGFILE = False try: opts, args = getopt.getopt(argv[1:], "l:a:p:L:ch?v") for opt in opts: opt, val = opt if opt == "-l": PORT = int(val) elif opt == "-a": REMOTE_HOST = val elif opt == "-p": REMOTE_PORT = int(val) elif opt == "-L": LOGFILE = val elif opt == "-c": CONSOLE = False elif opt == "-?" or opt == "-h": usage() elif opt == "-v": print "Python TCP/IP Spy Version 1.01 Copyright (c) 2009 by Alexander Demin" sys.exit(1) else: usage() if not PORT: raise StandardError, "listen port is not given" if not REMOTE_HOST: raise StandardError, "remote host is not given" if not REMOTE_PORT: raise StandardError, "remote port is not given" except Exception, e: print "error:", e, "\n" usage() # Remote host REMOTE = (REMOTE_HOST, REMOTE_PORT) # Create logging contitional variable log_cond = threading.Condition() queue = [] def logger(): global queue while 1: log_cond.acquire() while len(queue) == 0: log_cond.wait() if LOGFILE: try: logfile = open(LOGFILE, "a+") logfile.writelines(map(lambda x: x+"\n", queue)) logfile.close() except: pass if CONSOLE: for line in queue: print line queue = [] log_cond.release() # Thread safe logger def log(thread, msg): if CONSOLE or LOGFILE: log_cond.acquire() queue.append("%04d: %s" % (thread, msg)) log_cond.notify() log_cond.release() def printable(ch): return (int(ch < 32) and '.') or (int(ch >= 32) and chr(ch)) # Pre-build a printable characters map printable_map = [ printable(x) for x in range(256) ] # Thread safe dumper def log_dump(thread, msg): if CONSOLE or LOGFILE: log_cond.acquire() width = 16 header = reduce(lambda x, y: x + ("%02X-" % y), range(width), "")[0:-1] queue.append("%04d: ----: %s" % (thread, header)) queue.append("%04d: %s" % (thread, '-' * width * 3)) i = 0 while 1: line = msg[i:i+width] if len(line) == 0: break dump = reduce(lambda x, y: x + ("%02X " % ord(y)), line, "") char = reduce(lambda x, y: x + printable_map[ord(y)], line, "") queue.append("%04X: %04X: %-*s| %-*s" % (thread, i, width*3, dump, width, char)) i = i + width log_cond.notify() log_cond.release() # Spy thread def spy_thread(local, addr, thread_id): log(thread_id, "Thread started") try: log(thread_id, "Connecting to %s..." % str(REMOTE)) remote = socket.socket(socket.AF_INET, socket.SOCK_STREAM) remote.connect(REMOTE) except Exception, e: log(thread_id, "Unable connect to %s -> %s" % (REMOTE, e)) local.close() return LOCAL = str(addr) log(thread_id, "Remote host: " + LOCAL) try: running = 1; while running == 1: rd, wr, er = select.select([local, remote], [], [local, remote], 3600) for sock in er: if sock == local: log(thread_id, "Connection error from " + LOCAL) running = 0 if sock == remote: log(thread_id, "Connection error from " + REMOTE) running = 0 for sock in rd: if sock == local: val = local.recv(1024) if val: log(thread_id, "Recevied from %s (%d)" % (LOCAL, len(val))) log_dump(thread_id, val) remote.send(val) log(thread_id, "Sent to %s (%d)" % (REMOTE, len(val))) else: log(thread_id, "Connection reset by %s" % LOCAL) running = 0; if sock == remote: val = remote.recv(1024) if val: log(thread_id, "Recevied from %s (%d)" % (REMOTE, len(val))) log_dump(thread_id, val) local.send(val) log(thread_id, "Sent to %s (%d)" % (LOCAL, len(val))) else: log(thread_id, "Connection reset by %s" % str(REMOTE)) running = 0; except Exception, e: log(thread_id, ("Connection terminated: " + str(e))) remote.close() local.close() log(thread_id, "Connection closed") try: # Server socket srv = socket.socket(socket.AF_INET, socket.SOCK_STREAM) srv.bind(("", PORT)) except Exception, e: print "error", e sys.exit(1) counter = 1 threading.Thread(target=logger, args=[]).start() log(0, "Listen at port %d, remote host %s" % (PORT, REMOTE)) while 1: srv.listen(1) local, addr = srv.accept() log(0, "Connection accepted from %s, thread %d launched" % (addr, counter)) threading.Thread(target=spy_thread, args=[local, addr, counter]).start() counter = counter + 1
Лично я постоянно использую этот скрипт на Windows, Linux и Solaris.
Следующий шаг - это переписать все на чистом С в виде одного единственного файла, который можно было бы в течение минуты забросить на любой UNIX или Windows, скомпилить и получить готовую программу. Питон - это конечно здорово, но, например, для AIX или HP-UX Питон является небольшой загвоздкой, которую в пять секунд не решить.
А что стоит у вас на вооружении по этому вопросу?
]]>Выполнив задачи A (Alien Language), B (Watersheds) и малую размерность задачи C (Welcome to Code Jam) я успешно сел в лужу на большой размерности задачи С. У меня там и не пахло отведенными 8 минутами на прогон данных. Сейчас ищу проблему, но поезд ушел – на решение большой размерности дается только одна попытка.
Традиционный вывод: Good algorithms are better than supercomputers.
В итоге, пока я на почетном пенсионерском 1784-ом месте с 76-ю баллами (под именем begoon) из около 5700 всех участников.
Радует, что в топовой двадцатке три российских флага.
Хотя пишут, что для прохода в первый раунд надо только 33 балла, то есть фактически только одна полностью решенная задача, решения больших размерностей не проверяются в онлайне, и окончательная оценка будет только после окончания квалификации.
Забавно, через 3-4 часа после начала забега у них там что-то упало, и нельзя было скачать задания. Быстро все починили, но продлили время для участников на пару часов.
Кто-нибудь учавствует?
]]>std::string
, или таки надо реализовывать свой класс для строчек.
Один из вопросов - это вопрос качества самой реализации std::string
. Забавно, что большинство людей, которых я просил набросать прототип класса для строчек, более менее эффективного с точки зрения работы с памятью, писали примерно следующее:
class String { public: explicit String(const std::string& value) { init(value.c_str(), value.length()); } String(const String& value) { init(value.data_, value.sz_); } ~String() { free(data_); } String& operator=(const String& value) { if (this != &value) { if (value.sz_ > sz_) data_ = (char*)std::realloc(data_, value.sz_); sz_ = value.sz_; std::memcpy(data_, value.data_, sz_); } return *this; } private: void init(const char* data, size_t sz) { sz_ = sz; data_ = (char*)malloc(sz_); std::memcpy(data_, data, sz_); } char* data_; size_t sz_; };
Ясно, что при такой реализации оператора присваивания строка в плане занимаемой памяти будет только расти. Это сделано специально.
Практически никто сразу не думал о необходимости наличия операции перемещения, например swap
. Почему-то наличие конструктора копирования и оператора присваивания считается достаточным.
Что ответить на этот вопрос раз и навсегда для себя самого, я написал тестовую программу. Это программа сортирует массив из длинных строк. Строки представлены четырьмя способами: объект std::string
, указатель на std::string
, объект самопального класса String
(см. выше) и указатель на String
.
По предварительным очевидным оценкам работа через указатель должна быть максимально эффективная, так как в данном случае при перемещении объектов физически std::sort()
переставляет только указатели, а не сами объекты.
А вот при работе непосредственно с объектами будет интересно сравнить, насколько банальная реализации строки будет уступать std::string
.
Итак, std_string.cpp
:
#include <iostream> #include <sstream> #include <string> #include <vector> #include <algorithm> #include <cstdlib> #include <cstring> #include <cassert> #include "gtest/gtest.h" static const int N = 100; // Самопальный класс, реализующий хранение строки более менее // эффективным образом с точки зрения копирования. class String { public: // "explicit" запрещает неявное приведение аргумента, что мы // могли точно знать, какие конструктор каких классов вызываются. explicit String(const std::string& value) { init(value.c_str(), value.length()); } String(const String& value) { init(value.data_, value.sz_); } ~String() { free(data_); } // Данный оператор - это, пожалуй, единственная попытка сделать // работу с памятью эффективной. String& operator=(const String& value) { if (this != &value) { // Память перераспределяется только если оригинал длинее текущей // строки. Ясно, что при такой реализации строка может только // расти в плане занимаемой памяти. if (value.sz_ > sz_) data_ = (char*)std::realloc(data_, value.sz_); sz_ = value.sz_; std::memcpy(data_, value.data_, sz_); } return *this; } friend class StringCmp; friend class StringPointerCmp; private: void init(const char* data, size_t sz) { sz_ = sz; data_ = (char*)malloc(sz_); std::memcpy(data_, data, sz_); } char* data_; size_t sz_; }; std::vector<std::string> std_strings; std::vector<std::string*> std_strings_p; std::vector<String> strings; std::vector<String*> strings_p; // Объект для сравнения двух std::string. class StlStringCmp { public: bool operator()(const std::string& a, const std::string& b) { return a < b; } }; TEST(SortingStlString, StlString) { std::sort(std_strings.begin(), std_strings.end(), StlStringCmp()); } // Объект для сравнения двух std::string*. class StlStringPointerCmp { public: bool operator()(const std::string* a, const std::string* b) { return *a < *b; } }; TEST(SortingStlString, StlStringPointer) { std::sort(std_strings_p.begin(), std_strings_p.end(), StlStringPointerCmp()); } // Объект для сравнения двух String. class StringCmp { public: bool operator()(const String& a, const String& b) { assert(a.sz_ == b.sz_); return std::memcmp(a.data_, b.data_, a.sz_); } }; TEST(SortingStlString, String) { std::sort(strings.begin(), strings.end(), StringCmp()); } // Объект для сравнения двух String*. class StringPointerCmp { public: bool operator()(const String* a, const String* b) { assert(a->sz_ == b->sz_); return std::memcmp(a->data_, b->data_, a->sz_); } }; TEST(SortingStlString, StringPointer) { std::sort(strings_p.begin(), strings_p.end(), StringPointerCmp()); } int main(int argc, char* argv[]) { // Это наполнитель, чтобы строки были длинные, и копирование было // ощутимо дорого. std::string big(1024 * 1024, '?'); for (int i = 0; i < N; ++i) { // Все строки будут одинаковой длины. Функции сравнения рассчитывают // на это. std::stringstream fmt; fmt << N * 2 - i << big; // std::string строка-объект. std_strings.push_back(fmt.str()); // std::string строка-указатель. std_strings_p.push_back(new std::string(fmt.str())); // Моя строка-объект. strings.push_back(String(fmt.str())); // Моя строка-указатель. strings_p.push_back(new String(fmt.str())); } testing::InitGoogleTest(&argc, argv); // Принудительно печатаем время работы тестов. testing::GTEST_FLAG(print_time) = true; return RUN_ALL_TESTS(); }
Компилируем:
cl /O2 /EHsc /I. std_string.cpp gtest-all.cc
Запускаем:
[==========] Running 4 tests from 1 test case.
[----------] Global test environment set-up.
[----------] 4 tests from SortingStlString
[ RUN ] SortingStlString.StlString
[ OK ] SortingStlString.StlString (203 ms)
[ RUN ] SortingStlString.StlStringPointer
[ OK ] SortingStlString.StlStringPointer (0 ms)
[ RUN ] SortingStlString.String
[ OK ] SortingStlString.String (891 ms)
[ RUN ] SortingStlString.StringPointer
[ OK ] SortingStlString.StringPointer (0 ms)
[----------] 4 tests from SortingStlString (1125 ms total)
[----------] Global test environment tear-down
[==========] 4 tests from 1 test case ran. (1125 ms total)
[ PASSED ] 4 tests.
Видно, что версии с указателями отработали примерно одинаково быстро, а вот при работе с объектами std::string
обогнал самопальную реализацию в 4 раза - 203 мс против 891 мс.
Несложно понять, почему это так. std::sort()
для перестановки элементов использует шаблонную функцию std::swap()
, которая для std::string
реализована так, чтобы делать перестановку без физического копирования данных. А для String
все происходит банально через конструктор копирования и оператор присваивания.
В общем, для себя я вынес, что не надо городить свой огород, так как в большинстве случаев std::string
решает все проблемы. Но возникает вопрос - как добавлять в std::string
свою функциональность? Например, поиск слов.
Проблема в том, что у std::string
деструктор объявлен как невиртуальный (может это сделано по соображениям эффективности), а наследование от класса с невиртуальным деструктором в C++ является не самой правильной затеей.
Автор STL Александр Степанов в своем труде Notes for the Programming course at Adobe советует реализовать дополнительную функциональность для стандартных контейнеров STL через шаблонные алгоритмы. Плюсов тут много, например, реализовав какой-то разбор строки через итераторы в виде шаблонной функции-алгоритма, можно автоматически получить её же для все остальных контейнеров, у которых есть такие же итераторы.
Интересно, что пишет Степанов про свой взляд на то, как надо реализовывать функцию length()
для контейнеров (в документе, ссылка на который дана выше, Степанов показывает шаг за шагом процесс создания эффективного контейнера):
While we could make a member function to return length, it is better to make it a global friend function. If we do that, we will be able eventually to define the same function to work on built-in arrays and achieve greater uniformity of design. I made size into a member function in STL in an attempt to please the standard committee. I knew that begin, end and size should be global functions but was not willing to risk another fight with the committee.
Он считает, что глобальная шаблонная функция length()
- это правильнее, чем length()
как член класса. Если б не комитет стандартизации - так оно и было бы в STL.
Итак, подытоживая сказанное, не стоит не доверять std::string
. Для большинства задач этот класс решает все проблемы. Если надо добавить функциональность - это надо делать через реализацию шаблонного алгоритма.
Есть замечания или протесты? Прилагайте.
Посты по теме:
]]>Недавно я купил его последнюю книгу “Elements of Programming”.
Довольно своеобразная книга. Много математики, приводимой как обоснование тех или иных приемов в программировании (язык, конечно, С++), из-за чего читается немного тяжело. Забавно, я читал многие его публикации до этого, и как-то заметил, что чем дальше, тем больше он использует формальную математику для описания программирования.
Еще интересный факт: в данной книге везде при использовании шаблонов используются концепты, хотя недавно было принято решение, что в C++0x их не будет из-за общей пока недоработанности идеи.
Но вернемся к другим публикациям Степанова.
Одни из мои любимых - Notes for the Programming course at Adobe и Science of C++ Programming.
Например, ставший классикой, его пример на тему итераторов:
if (!v.empty()) { sort(&*begin(), &*v.begin() + v.size()); }
когда спрашивается, почему в данном вполне рабочем примере обязательно нужна проверка v.empty()
и почему нельзя второй аргумент нельзя записать как &*v.end()
?
Но вот что лично мне понравилось, это способ реализации оператора присваивания для класса. Обычно, когда в классе есть конструктор копирования и оператор присваивания, стандартный прием - это сделать закрытую функцию типа clone()
или copy()
, которая умеет правильно копировать класс, если внутренняя его структура нетривиальна, и вызывать эту функцию из конструктора копирования и оператора присваивания, тем самым избегая дублирования кода.
Но Степанов говорит следующее: “…присваивание должно осуществляться вызовом деструктора и последующим конструктором”. То есть надо просто сделать полноценный конструктор копирования, а оператор присваивание реализовать так:
T& T::operator=(const T& x) { if (this != &x) { this->T::~T(); new (this) T(x); } return *this; }
Получается, что старый объект сам себя разрушает, вызвав деструктор (но память под ним не освобождается), а затем оператором new
с явным размещением (память под объект тут уже повторно не распределяется) объект создается снова через конструктор копирования.
В общем, данные pdf-ки - очень интересное чтиво. Причем, Степанов никогда не забывает об эффективности (ибо неграмотное использования возможностей шаблонов элементарно делает программу очень медленной и жадной до ресурсов), и, например, главы про техники перемещения, а не копирования объектов очень познавательны.
]]>Вот книжный пример (default_values.cpp
):
#include <iostream> class X { public: virtual void f(int i = 0) { std::cout << "X::f(): " << i << std::endl; } }; class Y: public X { public: void f(int i = 1) { std::cout << "Y::f(): " << i << std::endl; } }; int main() { X* a = new Y; a->f(); Y* b = new Y; b->f(); return 0; }
Что распечатает эта программа? Вот что:
Y::f(): 0
Y::f(): 1
В обоих случаях была вызвана функция Y::f()
, но значение аргумента было разное.
Конечно, для понимающих механизм виртуальных функций тут нет никаких чудес и очевидно, что при перегрузке метода дочерний класс изменит только адрес в таблице виртуальных функций. Информации о значениях параметров по умолчанию взяться просто не откуда. Поэтому компилятор честно берет эту информацию из типа указателя.
Я намеренно опустил модификатор virtual
в описании функции f()
в классе Y
. Но от этого она невиртуальной не стала. При сложной иерархии классов очень несложно не заметить, что изначально функция была виртуальной, и для собственного удобства подправить ей значение параметра по умолчанию для какого-то конкретного случая, тем самым привнеся очень неприятную ошибку (и тут уже надежда на зоркий глаз коллеги при code view или на статические анализаторы).
При возрастающей сложности проекта гораздо важнее иметь надежные исходники, чем минутное локальное удобство.
Посты и ссылки по теме:
]]>Теперь ближе к делу. Blogspot как механизм для ведения блога меня вполне устраивает. Единственное, чего не хватает - это возможности форматирования постов не в HTML или WYSIWYG редакторе, а на каком-нибудь диалекте Wiki. Мне много-то не надо, хотя бы базовые элементы.
Последнее время я как-то уж сблизился с Google Code. А там как раз есть Wiki для ведения документации. Вот и возникала мысль скрестить Blogspot и Google Code для ведения блога.
Мой блог о программировании, поэтому задача удобного внедрения программного кода в посты стоит на первом месте. К тому же я люблю всегда давать по возможности полные компилируемые исходники, поэтому хостинг для исходников тоже нужен.
Я пришел вот к такой схеме. Для начала я завел одноименный проект на Google Code - Easy Coding.
Каждый пост пишется на Google Code в виде отдельной странички Wiki (лишний повод не писать двух-строчных одноразовых постов). Отлаживается верстка и общий внешний вид. Можно писать прямо в онлайне на страничке, но лично я изначально набиваю все в notepad++
и aspell
. Благо разметка Wiki не требует ежеминутного закольцованного ерзанья типа исправил-посмотрел-как-выглядит, как в HTML.
Если надо повесить картинку, то она выкладывается в раздел Downloads, и ссылка на картинку ведет туда. Исходники тоже можно было бы заливать в этот раздел, есть есть способ лучше. Исходники удобнее держать в разделе Source. Там их можно удобно просматривать, скачивать, комментировать и т.д. И тут уже можно насладиться контролем версий для них (еще раз уточню - когда выкладываешь работающие исходники, это становится важно) - Subversion или Mercurial, кому что больше нравится. Кстати, раздел Wiki тоже под контролем версий.
Я использую Mercurial, так как распределенная модель позволяет работать долго локально, а потом одним махом отправить изменения на сервер. Получается, что Mercurial в данном случае - это как файловый клиент для обмена файлами с Google Code.
Итак, пост закончен и отлажен. Теперь его надо выложить на Blogspot.
Для этого я написал элементарный скрипт на php. Данный скрипт преобразует пост из формата .wiki в .html с ориентацией на особенности шаблонов и стилей конкретно Blogspot’а. Он, конечно, не идеален, но я решил так - я не буду делать универсального монстра, а буду править его по мере возникновения проблем или требования новых возможностей.
Так так теперь у меня всегда локально лежат посты в виде .wiki
файлов, то если мне надо исправить пост - сначала я вношу изменения в .wiki
, затем конвертирую .wiki
снова в .html
и выкладываю через онлайновый редактор Blogspot’а. Это позволяет всегда иметь посты в нормальной .wiki
разметке и не думать, как их корежит онлайновый редактор Blogspot’а.
Соглашусь, многое из сказанного странно для владельцев собственных автономных блогов, но вот для клиентов Blogspot’а подобная методика весьма жизненна.
]]>Суть ее в следующем. Представьте, вы играете в следующую игру: перед вами три ящика, и в одном из них приз. Два остальных пустые. Вам надо угадать ящик с призом. Вы делаете первую попытку и наугад выбираете один ящик из трех, но ящик пока не открывают. Вместо этого ведущий игры берет и открывает один из двух оставшихся ящиков, и тот оказывается пустым. После этого ведущий вам предлагает возможность изменить первоначальный выбор в свете новой информации о пустом ящике.
Естественно, ведущий точно заранее знает где приз и заведомо открывает пустой ящик. Итак, вы изначально выбрали ящик, но потом ведущий открыл один из оставшихся и выяснилось, что он пустой. Перед вами выбор: оставить свой изначальный выбор неизменным или изменить его, выбрав третий ящик (тот, что остался после вашего первого выбора и после открытия ведущим пустого ящика). При какой стратегии вероятность выигрыша выше?
Самое прямолинейное решение, приходящее в голову: смена ящика ничего особенно не даст. Вы выбрали один ящик из трех - вероятность выиграть 1⁄3. После открытия одного ящика ведущим их осталось два, поэтому вероятность угадать где приз 50⁄50. Выбор вы уже сделали, и он и так является одним из текущих вариантов. Выходит, что нет особого смысла менять выбор.
Но эта задачка тем и интересна, что при столь тривиальной постановке ее правильное решение не совсем очевидно, хотя с точки зрения теории вероятности тут все прозрачно - теорему Байеса еще никто не отменял.
Правильный ответ - да, надо менять выбор, так как в этом случае вероятность угадать повышается с 1/3
до 2/3
(и даже не 1/2
).
В Википедии приведено исчерпывающее объяснение.
Ну а чтобы уж окончательно развеять все сомнения, пришлось провести эксперимент.
montihall.cpp
:
#include <iostream> #include <set> #include <cstdlib> #include <cassert> #include <ctime> int all_doors[] = { 1, 2, 3 }; bool no_change_strategy() { // doors - это множество доступных дверей (1, 2, 3) для выбора игроком. std::set<int> doors(all_doors, all_doors + 3); // Выбираем истинную дверь (от 1 до 3). int real_door = (std::rand() % 3) + 1; // Выбираем первый и окончательный выбор игрока (от 1 до 3). int primary_choice_door = (std::rand() % 3) + 1; return real_door == primary_choice_door; } bool change_strategy() { // doors - это множество доступных дверей (1, 2, 3) для выбора двери, // открываемой ведущим после первого выбора игрока. std::set<int> doors(all_doors, all_doors + 3); // Выбираем истинную дверь (от 1 до 3). int real_door = (std::rand() % 3) + 1; // Выбираем первый выбор игрока (от 1 до 3) int primary_choice_door = (std::rand() % 3) + 1; // Исключаем из множества дверей истинную дверь и выбор игрока. doors.erase(real_door); doors.erase(primary_choice_door); // На всякий пожарный проверим целостность данных. assert(doors.size() == 1 || doors.size() == 2); // Из оставшихся элементов (их может быть 1 или 2 штуки) выбираем дверь, // которую откроет ведущий. reveal_door равно либо 1, либо 2. int reveal_door = (std::rand() % doors.size()) + 1; // i указывает на первый элемент в множестве (всего в нем 1 или 2 элемента). std::set<int>::const_iterator i = doors.begin(); // Сдвигаем итератор на элемент, номер которого равен reveal_door. // Можно было бы написать "if (reveal_door == 2) ++i;", но цикл как-то // универсальнее. while (--reveal_door) ++i; reveal_door = *i; // 'doors2' - это множество доступных дверей (1, 2, 3) для игрока, // меняющего свой первоначальный выбор. std::set<int> doors2(all_doors, all_doors + 3); // Исключаем из множества дверей первый выбор игрока и // и дверь, открытую ведущим. doors2.erase(primary_choice_door); doors2.erase(reveal_door); // На всякий пожарный проверим целостность данных. assert(doors2.size() == 1); // В множестве оставшихся дверей будет только одна дверь, так как истинная // дверь точно не равна двери, открытой ведущим, во второй выбор игрока // точно отличается от первоначального. Поэтому просто берем из этого // множества первый элемент. int second_choice = *doors2.begin(); return real_door == second_choice; } int main() { std::srand(std::time(0)); int guess_on_change = 0; int guess_on_not_change = 0; int N = 100000; for (int i = 0; i < N; ++i) { if (change_strategy()) guess_on_change = guess_on_change + 1; if (no_change_strategy()) guess_on_not_change = guess_on_not_change + 1; } std::cout << "Вероятность выиграть при смене изначального выбора: " << guess_on_change * 1.0 / N << std::endl; std::cout << "Вероятность выиграть не меняя изначального выбора: " << guess_on_not_change * 1.0 / N << std::endl; return 0; }
Компилируем и запускаем:
cl /EHsc /D_NDEBUG montihall.cpp && montihall
Результат подтверждает теорию:
Вероятность выиграть при смене изначального выбора: 0.67005
Вероятность выиграть не меняя изначального выбора: 0.33347
Лично я провел замечательные несколько часов, вспоминая всю эту тему условных вероятностей. А вы?
]]>functor.cpp
:
#include "gtest/gtest.h" #include <algorithm> typedef double Type; Type* array; const int N = 100000000; inline bool less(Type a1, Type a2) { return a1 < a2; } class Less { public: inline bool operator()(Type a1, Type a2) { return a1 < a2; } }; // Использование встроенной функции сравнения. TEST(Callback, BuiltIn) { std::sort(array, array + N); } // Использование свободной функции сравнения по указателю. TEST(Callback, Function) { std::sort(array, array + N, less); } // Использование функтора. TEST(Callback, Functor) { std::sort(array, array + N, Less()); } int main(int argc, char* argv[]) { // Создаем отсортированный массив. array = new Type[N]; Type* p = array; for (int i = 0; i < N; ++i) *p++ = i; testing::InitGoogleTest(&argc, argv); // Принудительно печатаем время работы тестов. testing::GTEST_FLAG(print_time) = true; return RUN_ALL_TESTS(); }
Компилируем и запускаем (Visual Studio 2008):
cl /O2 /arch:SSE2 /EHsc /I. functor.cpp gtest\gtest-all.cc && functor
Результат:
[==========] Running 3 tests from 1 test case.
[----------] Global test environment set-up.
[----------] 3 tests from Callback
[ RUN ] Callback.BuiltIn
[ OK ] Callback.BuiltIn (9547 ms)
[ RUN ] Callback.Function
[ OK ] Callback.Function (24391 ms)
[ RUN ] Callback.Functor
[ OK ] Callback.Functor (9578 ms)
[----------] 3 tests from Callback (43547 ms total)
[----------] Global test environment tear-down
[==========] 3 tests from 1 test case ran. (43547 ms total)
[ PASSED ] 3 tests.
Видно, что скорость функтора (9578 мс) практически равна встроенной функции (9547 мс) сравнения. А вот вызов свободной функции конкретно отстает (24391 мс), приблизительно в 2.5 раза.
Такое поведение можно объяснить тем, что в данном случае при вызове обычной функции компилятор не может оптимизировать такой вызов встраиванием (inlining). Вне зависимости от того, что функция объявлена выстраиваемой, так как ее вызов производится по указателю, компилятор не может сделать предположений о значении этого указателя на стадии исполнения, а значит провести оптимизацию.
При использовании же функтора компилятору доступна информация о семантике вызываемого кода, поэтому все типы оптимизации возможны. Отсюда и скорость, близкая к встроенной функции сравнения.
Как вариант, при замене типа с double
на int
и при условии, что опция /arch:SSE2
включена, тест с функтором работал даже быстрее встроенной функции.
Вывод
Использование функторов предпочтительнее, чем свободных функций. С точки зрения проектирования и так все понятно (функтор, или функциональный объект, можно удобно тестировать, наследовать и т.д), но, как видно, и в плане производительности функтор тоже впереди.
]]>Google очень радеет за безопасность своих продуктов. Например, для браузера Chrome - это значит регулярные обновления. А кому охота каждый день качать по десять мегов, даже если там весьма критическое обновление. Поэтому разработчики Chrome делают все, чтобы сократить размер пересылаемых по сети данных.
Применение bsdiff частичное решает проблему, и уже не надо тупо слать архив целиком.
Но теперь к самому интересному. Что происходит, когда найден досадный баг и сделано исправление, например:
Было:
... if (packet_size < 1024) ... ...
а стало:
... if (packet_size > 10 && packet_size < 1024) ... ...
Исправили всего одну строчку. Но после компиляции и линковки результирующий файл будет радикально другой: съедет таблица символов, где-то изменится выравнивание, возможно линкер из-за этого изменит размещение сегментов и т.д. Море факторов, из-за которых самое минимальное изменение исходного текста приведет к глобальному изменению результирующего исполняемого файла.
Что делать? А что, если в обновлении посылать модуль не в двоичном виде, а в виде исходного текста? Тогда изменение (diff) будет совсем небольшое.
Конечно, посылать нормальный исходный текст и компилировать его на стороне клиента пока несколько затруднительно. Не у всех установлена Visual Studio.
Инженеры Google придумали технологию Courgette. Суть ее в том, что посылаются изменения на уровне не оригинального исходного текста, а на уровне ассемблера. Для формирования обновления скомпилированный исполняемый файл дизассемблируется. Тоже самое делается для предыдущей версии файла. Затем производится поиск изменений в текстовом виде (например, тем же стандартным diff
‘ом). И этот текстовый файл и является, собственно, обновлением. Затем он пересылается пользователю. На стороне пользователя текущий исполняемый файл тоже дизассемблируется, изменяется на основе принятого файла изменений (diff
‘а) и снова ассемблируется.
Старая схема с bsdiff
‘ом работала так:
сервер:
изменения = bsdiff(оригинал, обновление)
передать изменения
клиент:
принять изменения
обновление = bspatch(оригинал, изменения)
сервер:
ассемблер_оригинал = дизассемблировать(оригинал)
ассемблер_обновление = дизассемблировать(обновление)
ассемблер_обновление_кор = коррекция(ассемблер_обновление, ассемблер_оригинал)
ассемблер_изменения = bsdiff(ассемблер_оригинал, ассемблер_обновление_кор)
передать ассемблер_изменения
клиент:
принять ассемблер_изменения
ассемблер_оригинал = дизассемблировать(оригинал)
ассемблер_обновление_кор = bspatch(ассемблер_оригинал, ассемблер_изменения)
обновление = ассемблировать(ассемблер_обновление_кор)
Для успешной реализации дизассемблер должен располагать таблицей символов со стадии нормальной линковки исполняемого файла. Также на специальном шаге коррекция производится оптимизация ассемблерного кода для минимизации последующих различий с предыдущей версией путем группировки неизмененных частей.
А вот и результаты на примере единичного обновления Chrome’а:
Метод Размер обновления
----------------------------------------
Полное обновление 10,385,920
Обновление bsdiff'ом 704,512
Courgette метод 78,848
Неплохо, да?
Вот такой вот хитрый план у разработчиков Chrome. Обещают рассказать особенности реализации, как только все будет готово.
Данный пост является вольным переводом статьи, ссылка на которую дана в начале. Уж больно меня зацепила эта тема.
]]>Как всегда буду благодарен за замечания об ошибках, опечатках, и да вообще по стилистике в целом. Порой перевод технических терминов становится проблемой, так как с одной стороны для многих понятнее, когда используется просто английский термин (например, fixture), но с другой стороны хочется сделать жизнь как-то красивее и попытаться на своем родном писать без тотального засилия английских слов (их и так порядочно).
На отдельной странице будет общий список ресурсов о Google Test на русском языке. Очень пришелся кстати мой недавний пост о новшествах версии 1.3.0.
В планах перевод остальных документов.
В процессе работы над текстом перевода, у меня родилось вот такое небольшое лирическое отступление.
Я последнее время увлекся различными механизмами верстки при помощитекстовых языков разметки. И я не про HTML. Я говорю о таких вещах, как простые Wiki-диалекты, и заканчивая целым миром верстки под названием TeX. Периодически покуриваю на ночь документацию с по LaTeX. Начал я с совершенно дивного коротенького справочника, где сухо и сжато перечислены базовые элементы языка верстки LaTeX c примерами.
Например, моя любимая задача линейного программирования:
будет записываться так:
\[
\max_{n\in R}\sum_{j=1}^n
c_jx_j
;\;
\sum_{j=1}^n a_{ij}x_j\leqslant b_i, (i=1,\;2,\;\ldots,\;m)
\]
Если вы хотите в онлайне проиграться с набором формул в LaTeX, то рекомендую заглянуть сюда. Там, правда, не поддерживаются русские буквы в теле LaTeX команд, но для экспериментов это не проблема.
Когда я несколько лет назад верстал свою диссертацию в Ворде, я постоянно утыкался в корявости WYSIWYG подхода. Когда документ разрастается - сложная структура глав, много формул, картинок, внутренние и внешние ссылки, то уже крайне важной становится независимость содержания от, собственно, разметки. В WYSIWYG подходе на все случаи жизни уже не напасешься менюшек и кнопочек. Возникают корявости.
Безусловно, Ворд развивается семимильными шагами от версии к версии, оттачивая механизм стилей, без которого верстать что-то длинее десяти страниц вообще неудобно, но как же ему далеко от гибкости, которую придумали и реализовали в TeX десятки лет назад. К тому же чисто текстовое представление документа позволяет хранить его под контролем версий и удобно отслеживать изменения.
А объем и общая сложность документа моей диссертации была на порядки меньше любой технической книги даже среднего размера, особенно где много формул.
В общем, набивая перевод документации по Google Test, я просто отдыхал душой и попискивал от удовольствия, так как все происходило в Wiki-разметке в разделе документации проекта на Google Code.
Мечта - очень хотелось бы, чтобы когда-нидудь Blogspot реализовал бы альтернативный ввод постов с использованием Wiki-разметки, а не HTML. Хотя бы также, как это работает на Google Code. Ну а если еще и контроль версий будет, тогда вообще бесконечное счастье наступит.
Так если разобраться, единственная причина, почему нельзя вести блог прямо на Google Code в разделе Wiki - там нет трансляции постов в виде временной ленты. Если бы можно было в теле постов Блогспота на лету импортировать тест из документов Wiki c Google Code, то можно было бы удобно писать посты с применением Wiki-разметки и контролем версий, а отображать их в блоге.
Я, например, редко пишу в блог посты на 2-3 строчки. Обычно это крополивая работа по набивке, верстке, подгонке картинок, общего вида и т.д. И каждый пост это по сути законченный документ, к которому иногда возвращаешься, что-то исправляешь, добавляешь и т.д. И тут все прелести удобной итеративной работы с текстом выходят на первый план.
Посты по теме:
]]>Обычно я не комментирую новости, но тут не мог стерпеть.
]]>Для себя я его давно использую, а на работе удалось продавить его фрагменты в наш внутренний стандарт.
В качестве приятного бонуса Google раздает задорную утилитку cpplint, для быстрой проверки исходника на С++ на соответствие правилам и для генерации отчета, понимаемого средой разработки (например, Visual Studio). Написана она на Питоне, так что для ее использования его надо установить.
Я прикрутил cpplint
себе в Студию, чтобы можно было проверять исходники прямо в редакторе.
cpplint
имеет несколько десятков checker’ов, их можно опционально отключать. Я отключил только три:
#include
без указания относительного пути, например #include "one.h"
вместо #include "path/to/lib/one.h"
. Правило очень разумное, так как исключает перехлест заголовочных файлов с одинаковыми именами в разных подсистемах, но у меня уж больно много, где надо менять.#define
‘а в начале заголовочного файла. У меня свое правило именования, и оно меня устраивает.Итак, получился скрипт cpplint.cmd
:
C:\Python25\python.exe %~d0%~p0cpplint.py ^
--filter=-build/include,-build/header_guard,-readability/streams ^
--output=vs7 %1 %2 %3 %4 %5 %6 %7 %8 %9
Можно его из командной строки вызывать, но из Студии интереснее.
Итак, Menu->Tools->External Tool...
, жмем Add
и далее как на картинке (пути подправить по вкусу):
Теперь, прямо в редакторе жмем ALT-T,C,ENTER
и снизу окне результатов получаем отчет. Кликая на его строки можно скакать по исходнику.
Лично я считаю, что порядок в исходниках напрямую связан с порядком в голове его автора.
]]>В общем, я взял в руки:
Роберт Седжвик
Фундаментальные алгоритмы на C++. Части 1-4. Анализ. Структуры данных. Сортировка.
и начал освежать (а местами просто узнавать с нуля) когда-то читанное в студенчестве.
Втянулся.
Заказал вторую книгу:
Роберт Седжвик
Фундаментальные алгоритмы на C++. Часть 5. Алгоритмы на графах.
Последние несколько лет я всегда делал основной упор на новые языки, операционные системы, организацию процесса разработки, работу над качеством и т.д. Получилось, что база как-то слегка подкосилась.
В общем, если вы все еще можете не думая сказать, например, чем лучше хеш-таблица по сравнению со сбалансированным (например, красно-черным) деревом, почему именно Splay-дерево удобно для реализации кеша, написать по памяти пирамидальную сортировку или привести пример NP-задачи, которая не является полной, то может фундамент все еще крепок.
]]>assert
времени компиляции, если не использовать boost/static_assert.hpp
?
У меня вот такой:
template <bool> struct STATIC_ASSERTION_FAILURE; template <> struct STATIC_ASSERTION_FAILURE<true> {}; #define STATIC_CHECK(x) sizeof(STATIC_ASSERTION_FAILURE< (bool)(x) >)
Работает приемлемо сносно:
int main() { STATIC_CHECK(sizeof(int) < sizeof(char)); return 0; }]]>
easy-coding-2009.07.06-10.27.12.rar
Долгое время я использовал вот такой скрипт backup.cmd
:
rem Берем имя родительского каталога без полного пути. for %%I in (.) do set CWD=%%~nI rem Архивируем. winrar a -v -r -s -ag-YYYY.MM.DD-HH.MM.SS -x*.rar -x*.7z %CWD%
Просто бросаешь какой скрипт в каталог любого проекта (имя каталога должно быть сообразно проекту) и все, можно архивировать. Скрипт берет имя каталога как базу и добавляет к ней дату и время с помощью удобной опции архиватора RAR.
Последнее время я все чаще использую 7z как основной архиватор, но у него я не нашел схожего ключа на добавление в имя архива даты и времени. Пришлось слегка извратиться.
В этоге родился скрипт backup-7z.cmd
:
@echo off setlocal set line=%DATE% rem Проходимся по строке вида DD/MM/YYYY и rem превращаем ее в YYYY.MM.DD. :parse_date for /F "delims=/ tokens=1,*" %%a in ("%line%") do ( set line=%%b set now=%%a.%now% ) if "%line%" neq "" goto parse_date rem Отрезаем хвостовую точку от даты. set now=%now:~0,10% rem Добавляем время. Оно уже в формате HH:MM:SS.ms. Отрезаем доли секунды. set now=%now%-%TIME:~0,8% rem Заменяем двоеточие на точку set now=%now::=.% rem Берем имя родительского каталога без полного пути. for %%I in (.) do set CWD=%%~nI rem Архивируем. 7z a -mx9 -r -x!*.rar -x!*.7z %CWD%-%now%.7z endlocal
Это скрипт делает все как и раньше, но только для 7z.
Конечно, под UNIX’ом есть море путей сделать подобное, да и в Windows можно Cygwin использовать, но я всегда сначала пытаюсь сделать native решение, если это возможно.
]]>Проект называется lemonbind.
Lemon - это создающий исходник на C/C++ генератор, похожий на yacc или bison, для реализации заданой грамматики. Lemon был создан автором SQLite для разбора SQL’я.
Есть у Lemon несколько радикальных отличий от собратьев. В отличие от yacc/bison Lemon не использует обратные вызовы, то есть не парсер вызывает нас, когда готов принять очередной токен, а мы вызываем парсер, когда готовы скормить ему очередной токен. Также Lemon безопасен в потоковом плане и реентабелен. Именно эти отличия позволяют неплохо завернуть его в обертку C++ со всеми вытекающими радостями. Также Lemon использует более простую нотацию по именованию параметров в продукциях грамматики, значительно снижающую вероятность опечататься.
Lemon представляет собой всего для файла: lemon.c и lempar.c. Первый - это, собственно, генератор грамматик. Компилируется практически любым компилятором C/C++. Второй - шаблон, который использует генератор для создания целевого файла-парсера на языке С. Сгенерированный парсер также прекрасно компилируется любым компилятором C/C++.
Конечно, Lemon’у нужен лексический анализатор. Я использовал flex. Его тоже пришлось обернуть в С++.
Возникает вопрос - а для чего этот велосипед, когда есть ANTLR, Boost Spirit и прочие навороченные инструменты. Ответ как всегда прост - простота и скорость. Парадоксальная ситуация - подход с генерацией исходника, реализующего требуемую грамматику, был придуман много лет назад и воплощен в виде старичков типа lex/yacc/bison, а до сих пор используется за неимением простых и быстрых альтернатив, работающих на сложных грамматиках и на больших объемах анализируемого текста.
Собственно, моя мини библиотека на данный момент имеет три основных класса: Token, Tokenizer (обертка flex) и Parser (обертка Lemon). Набор тестов демонстрирует, как работать с этими классами. Тест Parser.NestedSelect разбирает вложенный SELECT тривиального диалекта SQL и строит дерево разбора.
Все находится в начальной стадии, но основной функционал уже присутствует. Пока не до конца продуман универсальный интерфейс для генерации дерева разбора (пока его генерация жестко привязана к конкретной грамматике), но сам парсер уже закончен.
P.S. Нашел дружественный проект по адаптированию Lemon’а для генерации парсеров не только на C и C++, но и на D.
P.P.S. Настоятельно рекомендую по теме вот эту книгу в заслуженной форме большего кирпича.
Альфред В. Ахо, Моника С. Лам, Рави Сети, Джеффри Д. Ульман
]]>На этот раз это эксперимент годовалой давности — удаленное управление для Lego NXT через апплет, работающий на сотовом телефоне.
Назвал незамысловато — nxtbtrc.
Все просто — запускается на телефоне апплет, он спаривается с NXT и потом может посылать на него команды. В целом ничего сложного, просто было интересно разобраться как работать с синим зубом в апплетах.
Врядли я там чего еще буду модифицировать, но может кому и пригодится.
Даже книжку, помню, для этого специально купил. Хорошая, кстати, книжка. Все доступно и понятно о bluetooth с точки зрения программиста. Рассмотрены несколько стеков разных производителей, их сравнение и использование на разных языках и платформах.
Albert Huang, Larry Rudolph, “Bluetooth Essentials for Programmers”
Обновление: Небольшое видео, демонстрирующее все в работе:
Посты по теме:
]]>Кстати, вопросы в конце тоже весьма и весьма полезны для обдумывания.
Лично для себя вынес, что code review в большинстве случаев гораздо эффективнее парного программирования.
Есть у меня мысль заправить это видео на наш очередной пятничный образовательный просмотр.
]]>В общем, я открыл файл algorithm
из STL’я в Visual Studio 2008 и часок покопался в нем. Вот результаты моих “исследований”.
Начнем с нестабильной сортировки std::sort().
QuickSort
(почти как у меня)if
‘ами)S = 1/8*N
) и для троиц элементов (1, S, 2*S)
, (N/2 - S, N/2, N/2 + S)
и (N - 2*S, N - S, N)
делается такая же минисортировка, как и на предыдущем шаге (где число элементов было меньше 40)QuickSort
процедура деления фрагмента на две части с использованием опорного элемента (цикл по перебрасыванию элементов, меньших опорного направо, а больших — налево)Количество рекурсивных операций не идет до победного конца, как в чистом QuickSort
. Если количество итераций (процедур разделения массива) превысило 1.5*log2(N)
, где N длина всего массива, то рекурсивные операции прекращаются. Если количество оставшихся недосортированных элементов меньше 32-х, то фрагмент досортируется методом вставки InsertionSort
(этот метод имеет общую сложность O(N2)
и для больших массивов не используется, но на малых длинах он быстрее всех из-за простоты). Если же остается более 32-х элементов, то досортировка происходит пирамидальным методом HeapSort
в чистом его виде.
Видимо все эти ухищрения для уменьшения накладных расходов QuickSort
на малых массивах.
Вот такая вот далеко непрямолинейная реализация.
Далее.
Стабильная сортировка std::stable_sort() реализована алгоримом слияния MergeSort
. Особых ухищрений по сравнению с чистным алгоритмом я не нашел. Разве что малые фрагменты (короче 32-х элементов) досортировываются методом вставки InsertionSort, как и в случае с QuickSort.
Частичая сортировка std::partial_sort() реализована в чистом виде пирамидальным методом HeapSort.
Вывод: Читать исходники очень интересно. Особенно хорошие исходники.
]]>Есть великое множество оберток Lua для С++, но я не нашел ни одной, где не надо вообще вызывать С-шные функции Lua вручную из основной программы. Также для создания новых функций на С++, которые можно будет вызывать из Lua, должен быть только С++‘ый подход.
Моя идея была в создании чисто плюсого интерфейса для Lua с максимально простой интеграцией в рабочий проект.
То, что пока вышло называется luascript.
Для включения в свой проект надо скопировать библиотеку в подкаталог luascript/
и добавить в проект два файла: luascript/luascript.cpp
и luascript/lua/lua-files.c
.
После этого можно писать вот такие куски кода:
lua script; script.set_variable<lua::string_arg_t>("a", "test"); script.exec("b = a .. '123';"); std::cout << script.get_variable<lua::string_arg_t>("b").value());
Данный простой скрипт принимает строку через переменню a
, добавляет к ней 123
и записывает результат в переменную b
, которая потом подхватывается из С++.
Если надо добавить свою функцию, например, для проверки существования файла, можно написать так:
class file_exists_func_t { public: // Регистрируем аргументы функции. В данном случае один аргумент типа "строка". static const lua::args_t* in_args() { lua::args_t* args = new lua::args_t(); args->add(new lua::string_arg_t()); return args; } // Регистрируем выходные параметры. В данном случае это просто bool. // Фукнция в Lua может возвращать не только одно значение, а несколько, // поэтому можно задать список типов выходных параметров. static const lua::args_t* out_args() { lua::args_t* args = new lua::args_t(); args->add(new lua::bool_arg_t()); return args; } // Задаем namespace и, собственно, имя фукнции. // Получается "fs.file_exits()". static const std::string ns() { return "fs"; } static const std::string name() { return "file_exists"; } // Данный метод вычисляет значение функции. // Сначала надо разобрать входные параметры, вычислить функцию и // положить результы с массив выходных значений. Правильность // работы с типами аргументов, выходных данных и индексов в массивах, // их описывающих, лежит на плечах автора функции. static void calc(const lua::args_t& in, lua::args_t& out) { std::string filename = dynamic_cast<lua::string_arg_t&>(*in[0]).value(); std::ifstream is(filename.c_str()); dynamic_cast<lua::bool_arg_t&>(*out[0]).value() = is.good(); } }; ... try { // Создаем исполнителя скрипта. lua script; // Регистрируем нашу функцию "fs.file_exists()". script.register_function< file_exists_func_t >(); // Устанавливаем переменную "fname" в "readme.txt". script.set_variable<lua::string_arg_t>("fname", "readme.txt"); // Вызываем скрипт. script.exec("exists = fs.file_exists(fname);"); // Получаем результат через переменную "exists". bool exists = script.get_variable<lua::bool_arg_t>("exists").value(); } catch (lua::exception& e) { std::cerr << "error: " << e.error() << ", line " << e.line(); }
Что пока не поддерживается, так это параметры типа таблица (хеш) для передачи их в функцию и получения их в качестве результата.
В каталоге lib
лежат несколько мини примеров на Lua. Например, вот так можно вызвать внешнюю функцию для base64
кодирования или декодирования:
lua script; script.exec("package.path = package.path .. ';./lib/?.lua'"); script.exec("require('base64'); a = base64.encode('test');"); // Данный пример напечатает "dGVzdA==". std::cout << script.get_variable<lua::string_arg_t>("a").value();
Исходники доступны для просмотра в онлайне, или через Mercurial.
Сборка.
Пока я проверял только в Студии 2008. Тестовый проект включает в себя библиотеку, lua 5.1.4, Google Test 1.3.0 и несколько тестов, чтобы почувствовать вкус библиотеки. Все в одном флаконе.
Те, у кого есть SCons, могут собрать, набрав scons -Q
. У кого нет, могут запустить скрипт compile-vs2008.cmd
. Собранный runner для тестов luascript_unittest_vs2008.exe
должен работать без ошибок. Посмотрев сами тесты в файле luascript_unittest.cpp
можно в целом понять, как работать с библиотекой. Документация, конечно, будет, но пока так.
Общие замечания.
Забавно, в этих исходниках я попытался в качестве эксперимента максимально работать по стандарту кодирования Google. Из основного, что затронуло лично меня, это:
public
, protected
, private
отступ в один пробел.{
практически всегда на той же строке (для классов, функций, циклов, условий и т.д.). Я раньше так не делал для классов и функций.cast
‘ов в стиле С, даже для элементарных типов. Только приведения в стиле С++. Мне это очень нравится.Это был снова эксперимент на Google Code и в opensource’e в целом. Если честно, то выкладывание исходников на публику страшно оздоравливает код, причем по всем статьям.
Данный проект не такой сухой как ранее выложенный SerialCom. Я с ним более менее активно работаю, так что должны быть точно улучшения. На работе, например, я его примастырил для гибко сконфирурированного фильтрования при журналировании. Есть, конечно, проблемы с производительностью (интерпретатор, все-таки, хоть и с виртуальной машиной), но есть пути улучшения.
Посты и ссылки по теме:
]]>SerialCom — программа для ковыряния в потоковых протоколах. Умеет работать с компортом, быть TCP/IP клиентом или сервером. Умеет удобно отображать и посылать шестнадцатеричные дампы. В довершение — проста как валенок.
В общем, когда-то мне нужна программа для удобной отладки устройства на PIC’е, с которым надо было работать по RS232. Ничего готового, подходящего мне по всем параметрам, я тогда не нашел, поэтому написал свою. Благо борландовые продукты располагают к пятиминутным двухкликовым проектам. Через некоторое время добавил работу с TCP/IP. Отлично подходит для возни с протоколами.
Проект очень прост. Кругом VCL и удобная компонента для работы с портом. Но это и обратная сторона медали — компилируется только в C++ Builder’е, причем из-за гениальной архитектуры компонент в VCL для сборки без допиливания нужен билдер именно версии 6.0. Использование более поздних сред потребует танцев по установки обновленной версии компортовой компоненты.
Особых планов на развитие проекта у меня пока нет, а использую программу я весьма часто, поэтому, проект отлично подошел для игр с хостингом.
Что понравилось.
Прежде всего, что теперь я могу использовать Mercurial, то есть распределенный контроль версий, а не Subversion. И кроме самих исходников отдельно отдается репозиторий wiki, что позволяет редактировать документацию также в офлайне.
Удобно, что сходу в довесок к хостингу получаешь возможность создать группу для обсуждений по проекту, привязку к единой статистике посещаемости сайта Google Analytics (просто надо UA указать), механизм code review и баг трекер впридачу. Хорошо не то, что все эти, в целом, обычные примочки есть, а хорошо, что они все связаны и даются одним кликом.
Что не понравилось.
Пришлось изменить оригинальное имя с SmartCom на SerialCom, так как первое уже занято. Но это так, ерунда.
Лично мне пока все нравится. Будем искать проблемы, а то без них как-то пресно.
]]>Весьма занимательное видео, рассказывающее некоторые подробности о внедрении Mercurial на Google Code. Почему именно Mercurial, а не Git или Bazaar, какие особенности именно у Mercurial, отличающие от конкурентов (я, например, не знал, в Mercurial хеш-идентификатор каждого коммита задействует не только метаданные, но и само содержимое файлов, что конкретно ограничивает возможности “переписывания” истории, хотя с точки зрения гугловцев это преимущество, нежели недостаток), и, собственно, как все это легло в инфраструктуру Google.
Посты по теме:
]]>Например, в школе, когда юный мозг так податлив и нежен, ничто так не уродует его как Бейсик, Рапира, Фокал, Лого или что-то там еще есть из разряда “простых языков для начинающих”. Лисп же совершенно без навязывания угловатых конструций типа циклов, условий, да вообще “программных строк” так таковых помогает сходу въехать в краеугольные темы типа рекурсии, списков, деревьев и т.д., которые так тяжело даются пониманию потом, когда в голове уже сидят классы, процедуры, функции с циклами впридачу. В Лиспе же можно без нудного прогружения в замыслование механизмы языке сходу переходить к делу - к структурам данных и алгоритмам. И получается, что их изучение идет параллельно с изучением языка программирования, а не с огромным запозданием. А это очень полезно для формирования правильного программерского мышления.
И пусть человек стопудово потом придет к обычным языкам, но его мозг уже будет иметь иммунитет на порой угловатые рамки любой императивщины. Сделать переход от функционального языка с императивному просто, а порой даже приятно, а вот наоборот - увы.
]]>Перечитал множество отзывов и сравнений, но ясно, надо попробовать все самому для своих задач.
Исторически Git для меня самая родная система, так как пользуюсь ей дольше всего. Поэтому пост будет во многом Git’оцентричный.
С Bazaar познакомился благодаря отличному блогу “Базарный день”.
Mercurial пришлось попробовать, так как это православно (причем весьма заслуженно).
Ни разу не претендую на глубину анализа или на попытку развязать холивар, а просто выскажу, что накопилось. Если я упустил какую-то возможность, пожалуйста поправьте меня.
О колокольне, с которой я смотрю на предмет. Я ищу систему не для дома (собственные проекты у меня сидят на разных системах, и все нормально), а для поддержки системы на нескольких видах UNIX плюс еще и Windows. Примерное количество файлов в ветке проекта около шести тысяч. Объем ~250 мегов (увы, есть некоторое количество двоичных файлов). Объем репозитория особо не волнует, если речь идет о разумных цифрах.
Git
Это скальпель. Время обучения и погружения в систему несколько больше, чем у конкурентов, но более менее приноровившись понимаешь, какой инструмент у тебя в руках. Начинающий пользователь с Git’ом в руках выглядит как ребенок, режущий колбаску хирургическим скальпелем.
Теперь по делу.
Что мне очень нравится в Git — это наличие staging системы (промежуточное звено между рабочими файлами и репозиторием). Очень удобно, когда можно подготовить для комита не весь файл, а только его часть.
Очень удобная система stashing для хранения временных наработок и переключения между ними без создания ветки, когда надо отвлечься на минуту для эксперимента.
Очень подкупает невообразимо мощная команда rebase, которой можно сделать с историей проекта все (в том числе и испортить). То есть Git не возводит в религию неприкосновенность истории. Механизм для ее модификации дается, но вот ответственность за результат перекладывается на пользователя. Никто не запретит тебе простым ключиком --amend
подправить синтаксическую ошибку в тексте последнего комита (да и любого другого комита) или удалить любой комит из истории, но вот надо тебе или нет — вопрос персонального подхода к работе. Из личного опыта как занимающего выпуском релизов скажу, что порой очень нужно иметь возможность менять историю, увы. В Perforce мне из-за этого приходится делать много ручной работы.
Под занавес — есть приятный бонус в виде качественного публичного хостинга github.com.
Из минусов, трогающих меня — это просто омерзительный порт под Windows. Я пользуюсь версией, построенной на MinGW. Пока непобежденным глюком для меня является тот факт, что по какой-то причине некоторые базовые утилиты UNIX, входящие в состав дистрибутива Git под Windows, при старте пытаются, видимо, определить наличие всех логических дисков (C:
, D:
и т.д.) в системе. Хорошо, когда нет сетевых дисков, а вот когда они есть, то такой опрос занимает раздражающие 2-3 секунды при каждом запуске (причина была выявлена путем анализа сетевого трафика, так как сначала я думал, что у меня вирус). На домашнем компьютере все отлично — там нет сетевых дисков.
Но несмотря на все препоны, с помощью git-p4
я наладил для некоторых наших разработчиков, работающих часто в офлайне, неплохой механизм интеграции с централизованным Perforce. Человек синхронизируется, будучи онлайн, и обновляет локальный репозиторий Git. Потом спокойно работает в офлайне через Git, а затем опять в онлайне засылает все сделанное из Git в Perforce.
Bazaar
Классная система. Работает с полоборота из коробки, но только там, где есть Питон, поэтому на некоторых наших вынужденных UNIXах меня ждал облом.
Пока я не нашел особых смысловых изъянов, мешающих мне работать.
Очень мне нравится подход, когда каждая ветка как таковая живет в отдельном каталоге, то есть имеет свой набор рабочих файлов (хотя может и не иметь).
Как-то с ходу не нашел бесплатного хостинга для Bazaar.
Mercurial
Снова Питон, поэтому автоматически все шоколадно на системах, где он есть, но грустно, где его нет.
Из хороших бонусов есть факт, что Google сделал поддержку хостинга для этой системы. Как написано в их отчете о том, почему они выбрали именно Mercurial, а не Git, как я понял, говорится, что основные причины в более простой интеграции Mercurial в систему http-сервисов (Git тоже умеет через http, но медленнее), и логическая близость синтаксиса команд Mercurial к Subversion (тут, конечно, Git ой как далеко).
В целом, для себя я решил пока так: если для дома для семьи или там где Windows да Linux, то это без сомнения Bazaar или Mercurial (можно монетку подкинуть), а вот все-таки для применения на множестве разнородных систем и там где надо уметь управлять историей, то пока Git.
]]>В UNIX в порядке вещей просто вызвать команду ps через popen() и распарсить текстовый вывод. Переносимо и надежно, так как для всех UNIXов ps всегда существует, и на этот факт можно положиться.
Для Windows же все оказалось чуть сложнее. Известная утилита pslist
не является стандартной, и полагаться на нее опасно. Возиться с Windows API тоже не хотелось.
Я нашел вот такой способ. Через _popen() (аналог UNIXового popen()
) можно вызвать вот такую команду:
WMIC PROCESS get Caption,Commandline,Processid
Получаем название процесса, командную строку и идентификатор процесса.
Конечно, не так задорно, как через ps, но зато стандартно.
]]>Хорошее введение в распределенную систему контроля версий Git от сообщества разработчиков ядра Линукса и от Линуса Торвальдса в частности.
Я ценю такие книги за начальное вовлечение в предмет. Это книга для начинающих, и если вы не новичок в области распределенных систем контроля версий, то вы ее проглотите за вечер и захотите более глубоких знаний по Git. Так и произошло со мной. Я прочитал книгу за вечер, она сформулировала в голове десятки неотвеченных вопросов и позволила мне понять — на какие вопросы мне нужны ответы.
На официальном сайте есть превосходная электронная живая книга по Git. Многие главы имеют дельные коротенькие видео уроки.
Вывод: обычно я не храню книги “для начинающих”, так как после них ты либо никогда предметом не интересуешься, либо копаешь глубже и знания для начинающих становятся очевидными. Но эту я оставлю.
P.S. Для интересующихся распределенными системами контроля версий рекомендую отличный блог про альтернативую систему Bazaar “Базарный день”. Прочтете несколько постов и будете готовы использовать Bazaar.
]]>Человеский фактор: успешные проекты и команды
Вы недавно помимо просто написания кода начали заниматься управлением командой?
Значит пока включить в свой книжный рацион раздел по управлению. И это одна из первых книг. Я прочитал ее не отрываясь за два вечера. Очень легко и понятно написано. Так как управление — это не программирование, и тут нельзя дать четкий рецепт “делай раз, делай два”, поэтому книги такого рода стоит рассматривать как повод для размызшлений о том, о чем вы, будучи просто программистом, никогда раньше не думали.
Вывод: Прочитал и поставил на близкую рабочую полку по соседству с Макконнеллом и Бруксом.
Посты по теме:
]]>Так как предварительный просмотр поста в Blogspot не дает полной картины, то единственный способ проверить вид поста, это его, собственно, запостить. Но кому охота выкладывать недоделанный пост? Итерация правки поста крайне неудобна и медленна — цикл “правка –> просмотр –> постинг –> ошибка –> правка –> …” очень коряв.
После некоторого времени использования Blogspot я пришел вот к такой схеме.
Для начала, нужен таки блог-клиент, как бы этого не хотелось. Тут тебе и вордовая проверка текста встроена, и preview какой никакой есть, и сохранение черновика и много прочих мелких радостей. Перебрав Semagic, BlogJet, несколько клиентов на Яве и придя от всего увиденного в ужас, а остановился, как ни странно на Microsoft Live Writer. Бесплатный (чудо для продукта от Microsoft), понимает Blogspot, много плагинов и встроенная вордовая проверка текста (то есть можно сразу в нем писать). В целом, почти все хорошо.
Но как бы не был хорош preview блог-клиента, единственный способ полностью проверить верстку в Blogspot — это запостить. Для этого можно завести еще один блог, тестовый, который должен быть точной копией основного блога в плане стилей и макета страницы. Теперь можно спокойно постить черновик в тестовый блог и отлаживать верстку окончательно. Причем цикл отладки выглядит так: редактирование в блог-клиента -> постинг в тестовый блог -> refresh страницы в браузере. Гораздо быстрее чем была, так как надо просто переключаться между окнами.
В конце отлаженный пост переносится в основной блог просто копированием html-кода.
]]>Наставление для программистов, руководящих другими программистами
Продолжая тему про книги для программистов, начавших заниматься управлением, расскажу про эту книгу.
Ее многие рекомендуют, поэтому я ее купил. Если честно, я мусолил ее несколько месяцев урывками (недавно добил таки). Крайне тяжелый и корявый язык (может это просто проблема плохого перевода?), читается поэтому очень муторно. Повсюду цитаты, повторяющиеся в самом тексте, что сбивает напрочь ход мысли. Порой автор говорит какую-то очевидную банальщину типа: Техническое лидерство взращивается на почве знания и питается готовностью учиться на собственных ошибках — в конечном итоге какая практика обязательно увенчается успехом. Можно заменить “техническое лидерство” на программирование, учебу, игру в шахматы или гольф и суть высказывания не изменится. Тогда о чем все этом?
В целом, лично мне книга не понравилась и рекомендовать ее я не буду. Может вам с ней повезет больше.
Посты по теме:
]]>Данный подход удобен всем. Разработчики в этом случае могу спокойно планировать свои спринты, распределяя по ним план развития продукта, а клиенты могут посмотреть на план этот развития (например, мы его публикуем на сайте поддержки) и понять, что и когда им ждать. Их планирование также упрощается.
Есть тут небольшая загвоздка – это исправление ошибок. Оценить время исправления коварного бага порой не так просто. Хорошо, что такие случаи, скорее всего исключения, так как если они становятся правилом, то возникает вопрос просто о качестве софта в целом.
Мы выпускаем релизы каждый месяц. Мне, как так называемому branch owner’у (я не очень люблю использовать английские слова, но вот «ответственный за версию» как-то уж очень коряво звучит), гораздо проще составлять списки патчей для включения в релиз и планировать окна тестирования, когда дата выпуска всегда постоянна. Конечно, интервал релизов подобран так, чтобы не приходилось выпускать пустые релизы без каких-либо патчей.
И каковы ваши наблюдения на этот счет?
]]>Лично я очень спокоен на тему стиля в целом и длины отступов в частности (лишь бы выглядело читабельно и соответствовало принятому для проекта соглашению), но вот табуляция — это как красная тряпка.
Ситуация усугубляется тем, что мы сопровождаем много кода, которому более двадцати лет, а тогда табуляция была еще ой как в ходу. Вот и приходится уговаривать людей проводить централизованные периодические зачистки кода от мусора прошлых лет, так как нет ничего хуже, когда человек совмещает смысловой коммит с “небольшими стилистическими правками”, типа удаления табуляций, разбиения длинных строк и т.д. В целом, в этом не ничего плохого, но после таких “небольших правок” слияние веток становится кошмаром.
]]>clean
-up и refresh
, а то все съедет” и т.д. Прелесть командной строки в том, что вне зависимости с какого ты сегодня будуна, и что твоя голова с утра проходит в дверь только боком, ты набираешь cd /my/super/project
и затем make
. После этого ты откидываешься на стул в мыслях о пивасике, а проект тем временем собирается и тестируется. В идеале, конечно, вместо компиляции, ты должен просто скачать свежую автоматизированную ночную сборку, которая уже там, оттестирована и готова к употреблению.
Ладно, это была всем очевидная лирика.
Наш софт представляет собой монструозный симбиоз из С, С++, Java, BASIC (это наш собственный внутренний СУБД-ориентированный язык), Python’а и UNIX-скриптов. Система же сборки основана на GNU Make и по сути является огромным многоуровневым Makefile’ом. Необходимая при сборке логика, которую нельзя реализовать напрямую в GNU Make дополняется UNIX-скриптами и мини утилитками на C, которые компилируются прямо перед запуском. Java части используют Ant. Плагин Ivy используется для подкачки из репозитория двоичных модулей. Лично я против каких-либо двоичных файлов в проекте, и считаю, что в разы удобнее все компилировать из текстового представления (пусть это и дольше по времени), так как текстовики можно сравнивать, в них можно искать и т.д. Конечно, реальная жизнь сложнее, и иногда приходится использовать заранее собранные бинарники (например, OpenSSL, ICU, тучу сторонних jar’ов для Java и т.д.).
Итак, ясно, такая сборка со временем деградирует, становится сложнее, запутаннее, в ней сложно искать ошибки, а тем более узкие места.
Я пытался все перевести на Ant – возможностей много, но все крайне Ява-центричное, расширения надо тоже писать на ней же. Если мы все писали бы на Java, но у нас не тот случай.
Пробовал CMake. Очень неплохо, но обнаружились сложности скрещения с нашим собственным компилятором Бейсика.
Пробовал SCons. Пожалуй, это самая прикольная система. Недаром ее используют в Гугле для реально нетривиальных проектов типа Chrome и Native Client и т.д. По сути Makefile
– это программа на Питоне, то есть ограничения на особую логику сборки (запуск тестов, фильтров, сборка документации, публикация результатов на FTP и т.д.) просто отсутствуют. Нужное просто пишется на полноценном языке программирования Питон. Удалось мне даже собрать нормально Питон для AIX и HPUX (с Windows, Linux, Solaris проблем нет вообще). Но и тут получилась ложна гов… дегтя. У меня есть необходимость конвертировать тысячи отчетов по тестам в формат jUnit. Мини утилитка на С, которая писалась на коленке, делает это менее чем за секунду. Все мои попытки на Питоне работали минуты. Получается, что идея опять не чиста, так как нужны опять сопровождающие утилиты, и уже не ясно, зачем что-то менять как оно есть сейчас.
В целом, мои изыскания в области идеальной утилиты организации сборки пока не увенчались успехом. Но поиск продолжается.
Другие посты по теме:
]]>Мы использует для этих нужд Hudson. Написано на Java, управляется через веб-интерфейс.
Несмотря на мою нелюбовь к Java, лично я мирно ужился с этой программой.
Что умеет Hudson, и для чего он нужен?
Hudson — это программа, которая умеет запускать скрипты локально или на удаленных машинах в зависимости от различных условий. Скрипты обычно производят сборку, тестирование, генерацию отчетов и документации, а события — это в основном поступление новых изменений в систему контроля версий.
Для начала, Hudson построен на плагинах, коих много, под разные запросы и желания.
Первый плагин, который нужен был конкретно нам — это плагин для работы с Perforce. Он умеет периодически опрашивать Perforce о наличии новых commit’ов и инициировать некоторые события, например сборку или запуск тестов.
Далее, ключевой момент для нас. Hudson имеет master-slave архитектуру, что позволяет с одного головного компьютера (master) проводить компиляцию, тестирование, архивирование и т.д. на множестве slave-машин. Так как наш софт поддерживается на нескольких разных типах UNIX и на Windows, по подобный подход очень упрощает управление таким зоопарком сборочных машин. Время разворачивания очередного slave’а — несколько минут (копирование slave.jar
, запуск java –jar slave.jar
и прописывание адреса этого slave’а на головной машине).
Еще мы активно используем плагины для посылки извещений по электропочте и для наглядного отображения результатов тестирования из формата jUnit. Наш софт состоит из смеси С, С++ и Java, поэтому пришлось выбрать единый формат представления журналов тестирования. Остановились на формате jUnit.
Каждая задача в Hudson может выполняться как на самом мастере, так и на заданном множестве slave-машин. Также задача может являться условием запуска другой задачи. Например, если задача компиляции проекта прошла успешно, то инициируется задача тестирования. Естественно, на каждом этапе можно архивировать промежуточные результаты (логи, тестовые файлы и т.д.), к которым можно всегда вернуться позже.
Наш сценарий использования Hudson таков: каждые пять минут Hudson опрашивает Perforce. Если в какой-то ветке появились новые изменения, то запускается “чистая” сборка ветки с новыми кусками кода. Каждая такая сборка снабжается файлом, в котором перечислены изменения по сравнению с предыдущей сборкой (changelog). Если сборка прошла успешно, по запускается набор функциональных и приемочных тестов. Кроме этого каждую ночь делается сборка со всеми изменениями за день. Если тесты прошли успешно, что результат архивируется в виде очередного ночного билда и выкладывается на ftp.
Если какая-то задача оканчивается сбоем (например, компиляция, так как кто-то “сломал” сборку, забыв проверить новый код на другой платформе, или какой-то из тестов не работает), то посылаются извещения ответственным лицам и также виновнику сбоя.
При нашей интенсивности commit’ов крайне редко за пять минут появляются более одного. Чем это удобно? А тем, что если при очередной сборке кто-то сломал функциональный или приемочный тест, сразу выясняется кто и как это сделал.
В целом, Hudson позволил нам сделать такую универсальную консоль в виде веб-странички, на которой в одном месте сразу видно все, что происходит в разработке, начиная от состояние функциональных и приемочных тестов, отчетов по покрытию и анализу качества кода и заканчивая списком незакрытых инцидентов в каждой ветке продукта.
А теперь реальный пример. В какой-то момент бета-тестеры начали сообщать, что система стала “как-то тормозить”. Точного момента никто не засек, а последняя “нормальная быстрая” сборка, которую эти тестеры имели, была сделана месяц назад. За этот месяц в ветку было внесено полсотни commit’ов. Искать среди них проблемный было бы занятием скучным (откат на определенную ревизию, сборка, тестирование, сравнение и т.д.).
Меня выручил Hudson. Я просмотрел отчеты по прогонам функциональных тестов за этот месяц и буквально сразу обнаружил, что в определенный день тесты сетевой подсистемы стали работать заметно медленнее. Область поиска сразу сузилась до четырех commit’ов сделанных в этот день. И только один из них был в сетевой подсистеме. Автор сего “улучшения” тоже нашелся сразу. Оказывается, человек что-то там оптимизировал в целях ускорения, а вышло наоборот. Итого, около часа на полное разбирательство в проблеме, включая перебрасыванием электропочтой с участниками инцидента. Я думаю, ручной поиск занял был день-два.
Вывод — удобная и максимально автоматизированная система фоновой интеграции является такой же важной частью групповой программной разработки, как и багтрекер и контроль версий.
А вы какими программами пользуетесь для автоматической интеграции?
Другие посты по теме:
]]>Я предложу, как мне кажется, очень простой и очень переносимый способ.
Стандартная функция time()
возвращает так называемое UNIX-время в секундах. Проблема в том, что секунда, номер которой возвращает эта функция, может быть уже через пару микросекунд перейдет на следующую. Надо как-то “подравняться” к границе секунд.
Фрагмент кода, в котором рабочий цикл имеет условие, позволяющее ему работать время, близкое к одной секунде:
... // Получаем номер текущей секунды time_t started = time(NULL); // Ждем перехода на следующую секунду while (time(NULL) == started); // И сразу запускаем рабочий цикл started = time(NULL); do { // Цикл, работающий в течение секунды ... } while (time(NULL) == started); ...
Тут, конечно, есть недостатки. Подготовительный цикл ожидания перехода на следующую секунду может “есть” процессорное время, если time()
для вашей системы не отдает time slice. Также сложно сделать какой-то надежный универсальный шаблон или макрос, так как надо гарантированно избежать какого-либо лишнего кода, чтобы не терять точность.
В целом, такой прием дает рабочему циклу работать время, очень близкое к секунде.
Если знаете, как сделать еще проще — предлагайте.
Другие посты по теме:
]]>Еще одним мощнейшим подспорьем является тестирование. Есть различные виды тестирования — unit-тестирование, функциональное тестирование, регрессивное тестирование и т.д.
Что понять, насколько хорошо проект покрыт тестами, нужна какая-то количественная мера. Например, это может быть количество предопределенных пользовательских сценариев, которые должны работать как задумано. Это неплохой показатель, и он обычно является основной мерой функционального тестирования и в целом отправной точкой в принятии решения о готовности релиза. Проблема этого подхода, что сами сценарии определены людьми, а значит являются условным и могут содержать ошибки и неточности. Хочется чего-то более объективного и более беспристрастного.
Одним из таких показателей может является количество строк кода, которые были отработаны (выполнены) в процессе тестирования. Эдакая мера для черных дыр в коде, которые никогда не выполняются обычно, а когда таки до них доходит, то все падает. Этот подход вовсе не отменяет функциональное тестирование, а органично дополняется его.
Итак, задача — надо понять, какие части программного коды были задействованы (были выполнены хотя бы раз) в процессе тестирования.
Представим ситуацию, что тестерам дали задание написать функциональные тесты для новой версии API на основе unit-тестов, написанных программистами, и на основе ожиданий заказчика от этого API. Они написали. А как понять, насколько полно они задействовали своими тестами все укромные уголки кода? Нужен какой-то инструмент.
Мы в компании остановились на Bullseye Coverage. Относительно небольшая цена (для сравнения с Coverity, которая стоила нам несколько десятков кило-зеленых на год, хотя это того стоит). Можно получить тестовый временный ключ для того, чтобы поиграться перед покупкой. Система поддерживает множество основных платформ.
Bullseye Coverage работает на уровне компилятора. Все что нужно — это активировать ее перед компиляцией проекта. После этого бинарные модули проекта будут сохранять в специальном файле статистику по собственной работе (чем-то похоже на работу профилировщика). Откомпилировали, запустили тесты (любые) и посмотрели — какие строки кода были реально выполнены этими тестами.
Bullseye Coverage может показывать задействование на уровне файлов/модулей, функций/классов и просто строк. Открыв файл исходного текста после прогона тестов специальным просмотрщиком можно, например, сказать, что эта конкретная строка или эта функция никогда не вызывалась в процессе тестирования. Порой это очень впечатляет.
Единственное, чего Bullseye Coverage не умеет, так это делать сравнительный анализ нескольких сборок, чтобы бы можно было отследить изменения показателей, а не просто иметь их абсолютные величины.
Лично меня результаты анализа некоторых наших проектов очень впечатлили и, порой, озадачили.
А вас?
Другие посты по теме:
]]>Файл trigger.h
:
#ifndef _EXT_TRIGGER_H #define _EXT_TRIGGER_H #ifdef WIN32 #include <windows.h> #else #include <pthread.h> #endif namespace ext { class Trigger { public: Trigger(); ~Trigger(); // Функция посылки сигнала потоку, // ждущему на функции Wait(). void Signal(); // Функция ожидания сигнала. // Вызов этой функции приводит к блокировке потока до // получения сигнала от функции Signal(). // Внимание: функция Signal() не должна быть вызвана до // того, как ждущий поток "сядет" на Wait(). Подобное // использование ведет к неопределенному поведению. void Wait(); private: #ifdef WIN32 HANDLE __handle; #else pthread_mutex_t __mutex; pthread_cond_t __cv; #endif // "Защита" от случайного копирования. Trigger(const Trigger&); void operator=(const Trigger&); }; } // namespace ext #endif
Файл trigger.cpp
:
#include "Trigger.h" namespace ext { #ifdef WIN32 Trigger::Trigger() { __handle = CreateEvent( NULL, // Атрибуты безопасности по умолчанию. TRUE, // Режим ручной активации события. FALSE, // Начальное состояния -- неактивное. NULL // Безымянное событие. ); } Trigger::~Trigger() { CloseHandle(__handle); } void Trigger::Signal() { SetEvent(__handle); } void Trigger::Wait() { // Ждем наступление события. WaitForSingleObject(__handle, INFINITE); // "Перезаряжаем" событие. ResetEvent(__handle); } #else // WIN32 Trigger::Trigger() { pthread_mutex_init(&__mutex, NULL); pthread_cond_init(&__cv, NULL); } Trigger::~Trigger() { pthread_cond_destroy(&__cv); pthread_mutex_destroy(&__mutex); } void Trigger::Signal() { pthread_mutex_lock(&__mutex); pthread_cond_signal(&__cv); pthread_mutex_unlock(&__mutex); } void Trigger::Wait() { pthread_mutex_lock(&__mutex); pthread_cond_wait(&__cv, &__mutex); pthread_mutex_unlock(&__mutex); } #endif // WIN32 } // namespace ext
Пространство имен, как обычно, ext, так что меняете по вкусу.
Проверим, как будет работать (естественно, через тест).
Для тестирования также потребуются: класс Thread, класс PreciseTimer и Google Test. О том, как собрать себе компактную версию Google
Test в виде всего двух файлов gtest-all.cc
и gtest.h
уже писал.
Файл trigger_unittest.cpp
:
#include <gtest/gtest.h> #include "trigger.h" #include "thread.h" #include "pretimer.h" // Тестовый поток, который будет "скакать" по указанным ключевым // точкам, увеличивая значение счетчика. class TriggerThread: public ext::Thread { public: TriggerThread(volatile int& flag, ext::Trigger& trigger) : __flag(flag), __trigger(trigger) {} virtual void Execute() { // Ждем первого сигнала. __trigger.Wait(); __flag = 1; // Ждем второго сигнала. __trigger.Wait(); __flag = 2; // Ждем третьего сигнала. __trigger.Wait(); __flag = 3; } private: volatile int& __flag; ext::Trigger& __trigger; }; TEST(Trigger, Generic) { volatile int flag = 0; ext::Trigger trigger; // Создаем поток и запускаем егою TriggerThread a(flag, trigger); a.Start(); // Подождем, чтобы поток "сел" на Wait(). ext::PreciseTimer::sleepMs(10); // Флаг не должен стать 1, так как поток // должен ждать на Wait(). EXPECT_EQ(0, (int)flag); // Информируем поток о событии. trigger.Signal(); // Подождем, чтобы поток успел изменить флаг на 1. ext::PreciseTimer::sleepMs(10); // Проверим, как он это сделал. EXPECT_EQ(1, (int)flag); // Далее проверка повторяется еще пару раз, чтобы проверить, // что синхронизирующий объект правильно "взводится" после // срабатывания. trigger.Signal(); ext::PreciseTimer::sleepMs(10); EXPECT_EQ(2, (int)flag); trigger.Signal(); a.Join(); // Последняя проверка не требует ожидания, так как мы присоединись // к потоку, и он точно уже завершился. EXPECT_EQ(3, (int)flag); }
Компилируем для Windows в Visual Studio:
cl /EHsc /I. /Fetrigger_unittest_vs2008.exe /DWIN32 runner.cpp ^
trigger.cpp trigger_unittest.cpp pretimer.cpp thread.cpp gtest\gtest-all.cc
или в GCC:
g++ -I. -o trigger_unittest_vs2008.exe runner.cpp \
trigger.cpp trigger_unittest.cpp pretimer.cpp thread.cpp gtest/gtest-all.cc
Запускаем:
[==========] Running 1 test from 1 test case.
[----------] Global test environment set-up.
[----------] 1 test from Trigger
[ RUN ] Trigger.Generic
[ OK ] Trigger.Generic (31 ms)
[----------] 1 test from Trigger (47 ms total)
[----------] Global test environment tear-down
[==========] 1 test from 1 test case ran. (78 ms total)
[ PASSED ] 1 test.
Работает.
Внимательный читатель заметит, что по хорошему бы надо протестировать случай, когда функция Signal()
вызывается раньше, чем слушающий поток дойдет до Wait()
. Как сказано в комментариях, эта ситуация считается логической ошибкой и ведет к неопределенному поведению. В жизни получается так: реализация для Windows считает, что если функция Signal()
была вызвана до Wait()
, то Wait()
просто тут же выходит, как бы получив сигнал сразу при старте. Реализация же под UNIX работает иначе: Wait()
отрабатывает только те вызовы Signal()
, которые были сделаны после начала самого Wait()
‘а. Самое настоящее неопределенное поведение. При использовании данного класса надо помнить об этом ограничении.
Другие посты по теме:
]]>Радостно, что авторы воплотили мою идею, когда вся библиотека собирается всего в два файла: gtest-all.cc
и gtest.h
. Теперь для этого есть специальный скрипт на Питоне. Распаковываем архив gtest-1.30.zip
и запускаем:
python scripts\fuse_gtest_files.py . fuse
После этого во вновь созданном подкаталоге fuse будет находиться “упакованная” версия библиотеки в виде двух файлов gtest/gtest-all.cc
и gtest/gtest.h
. Моя аналогичная, но ручная сборка для предыдущей версии больше неактуальна.
Опять таки приятно, что включили мой микропатч для возможности установки флагов командной строки прямо в исходниках тестов. Это очень удобно. Например, есть возможность печати времени работы тестов. Но по умолчанию эта функция выключена, и для ее включения надо в командной строке сказать --gtest_print_time
. Неудобно постоянно таскать за собой этот ключ. Теперь же можно прямо в тексте тестов, например, в головном модуле, задать этот параметр:
#include "gtest/gtest.h" int main(int argc, char* argv[]) { testing::InitGoogleTest(&argc, argv); testing::GTEST_FLAG(print_time) = true; return RUN_ALL_TESTS(); }
Итак, новые возможности версии 1.3.0:
--gtest_also_run_disabled_tests
для принудительного запуска отключенных тестовНебольшая программа ниже демонстрируем новые “вкусности” Google Test.
Файл runner.cpp
:
#include "gtest/gtest.h" #include <fstream> #include <iostream> #include <cstdlib> // ------------------------------------------------------- // Данная функция, если файл не существует, печатает сообщение // об ошибке и завершает программу с ненулевым кодом. void openfile(const char* name) { std::ifstream is(name); if (!is) { std::cerr << "Unable to open the file" << std::endl; std::exit(1); } } // Тест для функции openfile(). TEST(OpenFileDeathTest, ExitIfNoFile) { // Задаем заведомо несуществующий файл и смотрим - завершилась // ли программа с ненулевым кодом. Также проверяем регулярным // выражением то, что программа напечатала при выходе. // Мы ожидаем слово "open" среди остального вывода. ASSERT_DEATH({ openfile("__nofile__"); }, ".*open.*"); } // ------------------------------------------------------- // Данная функция должна падать с assert'ом, если делитель // равен нулю. int divide(int a, int b) { assert(b != 0); return a / b; } // Тест для assert'а в функции divide(). TEST(AssertDeathTest, DivideByZero) { // Задаем нулевой делитель и смотрим - упала или нет. // Вывод программы при падении не проверяем. ASSERT_DEATH({ divide(1, 0); }, ""); } // ------------------------------------------------------- // Данная функция должна при ненулевом коде завершать // программу, прибавив к заданному коду ошибки 50. void abandon(int code) { if (code != 0) std::exit(code + 50); } // Тест для функции abandon(). TEST(AbandonDeathTest, ExitCode) { // Вызываем функцию и смотрим код возврата. // Вывод программы при выходе не проверяем. ASSERT_EXIT(abandon(200), testing::ExitedWithCode(250), ""); } // ------------------------------------------------------- // Заведомо неработающий “сломанный” тест. // Если имя группы тестов или теста в отдельности предварить // словом DISABLED_, то тест не будет участвовать с запуске. // Это удобно, когда какой-то тест сломан, времени на его // отладку нет, но убирать его из тестирования совсем нельзя. // В это случае его можно отключить. Google Test при каждом // запуске будет напоминать, сколько имеется отключенных тестов. // В процессе же работы над тестом можно запускать программу // с параметром "--gtest_also_run_disabled_tests", который // будет проверять также и отключенные тесты. TEST(BadTest, DISABLED_Test) { FAIL(); } // ------------------------------------------------------- int main(int argc, char* argv[]) { testing::InitGoogleTest(&argc, argv); // Принудительно печатаем время работы тестов. testing::GTEST_FLAG(print_time) = true; return RUN_ALL_TESTS(); }
Компилируем в Visual Studio:
cl /EHsc /I. /Ferunner_vs2008.exe /DWIN32 runner.cpp gtest\gtest-all.cc
Запускаем:
[==========] Running 3 tests from 3 test cases.
[----------] Global test environment set-up.
[----------] 1 test from OpenFileDeathTest
[ RUN ] OpenFileDeathTest.ExitIfNoFile
[ OK ] OpenFileDeathTest.ExitIfNoFile (31 ms)
[----------] 1 test from OpenFileDeathTest (31 ms total)
[----------] 1 test from AssertDeathTest
[ RUN ] AssertDeathTest.DivideByZero
[ OK ] AssertDeathTest.DivideByZero (31 ms)
[----------] 1 test from AssertDeathTest (31 ms total)
[----------] 1 test from AbandonDeathTest
[ RUN ] AbandonDeathTest.ExitCode
[ OK ] AbandonDeathTest.ExitCode (32 ms)
[----------] 1 test from AbandonDeathTest (32 ms total)
[----------] Global test environment tear-down
[==========] 3 tests from 3 test cases ran. (94 ms total)
[ PASSED ] 3 tests.
YOU HAVE 1 DISABLED TEST
Отлично, все работает. Также не забудем, что у нас таки есть один отключенный тест. Его можно запустить принудительно, использовав ключ --gtest_also_run_disabled_tests
:
runner_vs2008.exe --gtest_also_run_disabled_tests
Получим следующее:
[==========] Running 4 tests from 4 test cases.
[----------] Global test environment set-up.
[----------] 1 test from OpenFileDeathTest
[ RUN ] OpenFileDeathTest.ExitIfNoFile
[ OK ] OpenFileDeathTest.ExitIfNoFile (31 ms)
[----------] 1 test from OpenFileDeathTest (31 ms total)
[----------] 1 test from AssertDeathTest
[ RUN ] AssertDeathTest.DivideByZero
[ OK ] AssertDeathTest.DivideByZero (32 ms)
[----------] 1 test from AssertDeathTest (32 ms total)
[----------] 1 test from AbandonDeathTest
[ RUN ] AbandonDeathTest.ExitCode
[ OK ] AbandonDeathTest.ExitCode (31 ms)
[----------] 1 test from AbandonDeathTest (31 ms total)
[----------] 1 test from BadTest
[ RUN ] BadTest.DISABLED_Test
runner.cpp(72): error: Failed
[ FAILED ] BadTest.DISABLED_Test (0 ms)
[----------] 1 test from BadTest (0 ms total)
[----------] Global test environment tear-down
[==========] 4 tests from 4 test cases ran. (94 ms total)
[ PASSED ] 3 tests.
[ FAILED ] 1 test, listed below:
[ FAILED ] BadTest.DISABLED_Test
1 FAILED TEST
Под занавес отмечу, что появился еще один новый ключ командной строки --gtest_help
для печати на экран всех весьма многочисленных параметров Google Test.
Я уже обновился до версии 1.3.0, а вы?
]]>Для включения/выключения proxy надо лазать в меню, что долго и неудобно. Лично мне удобнее просто скрипт запустить.
Кстати, гугловский Chrome по каким-то причинам использует настройки интернета от IE (системные для всего Windows), поэтому все сказанное актуально и для него.
Итак, привожу два скрипта для включения и отключения proxy в системных настройках интернета в Windows.
Файл iepon.cmd
:
reg add "HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Internet Settings" /v ProxyEnable /t REG_DWORD /d 1 /f
Файл iepoff.cmd
:
reg add "HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Internet Settings" /v ProxyEnable /t REG_DWORD /d 0 /f
Я проверял это только в Windows XP.
]]>Потом можно для снятия умственного напряжения и для еще большого подняния самооценки полистать сообщество “Programming WTF”.
Начав с известной нетленки для проверки условия i < 10
:
uint i; ... if (i.ToString().Length == 1) { ... }
можно постепенно усиливать ощущения…
std::string str1; std::string str2; ... if (!strcmp(str1.c_str(), str2.c_str())) { ... }
вставляя в код противопехотные мины…
различного радиуса поражения…
#define bool BOOL
и убойной силы.
<? define( "FALSE", -1 ); define( "TRUE", 0 ); ?>
А вот это для настоящих гурманов и знатоков своего дела:
#define sizeof(x) rand()
После того, как вы, обойдя вашу систему ревизий кода, чтобы никто не заметил засады, добавили это в какой-нибудь тихий, но повсеместно используемый файл ваших коллег смело идите покурить. Не думаю, что удасться выкурить в тишине хотя бы одну сигарету.
Теперь ваши коллеги тоже снимут стресс и напряжение.
]]>class T {...}; ... T t = T(1);
По очевидной логике вещей данный код должен при создании экземпляра класса T
вызвать конструктор по умолчанию (без аргументов), затем создать временный объект с помощью конструктора с одним аргументом и скопировать его в исходный объект перегруженным оператором копирования (или может конструктором копирования? ведь слева и справа объекты явно типа T
…).
К сожалению, тут невозможно просто догадаться по логике, тут надо знать, как это прописано в стандарте. Все эти “тонкости” конечно очевидны для профессионала, но у начинающих это порой вызывает непонимание, и как следствие использование однажды опробованных штампов “так работает” без какой-либо попытки что-то изменить.
Именно для таких случаев я обычно даю следующий пример, который покрывает часто используемые варианты создания объектов. Разобрав его один раз целиком, можно использовать его как подсказку в будущем, когда опять возникает вопрос “а что ж здесь будет вызвано: конструктор или оператор копирования?…“.
Итак, файл ctor.cpp
:
#include <iostream> class T { public: T() { std::cout << "T()" << std::endl; } T(int) { std::cout << "T(int)" << std::endl; } T(int, int) { std::cout << "T(int, int)" << std::endl; } T(const T&) { std::cout << "T(const T&)" << std::endl; } void operator=(const T&) { std::cout << "operator=(const T&)" << std::endl; } }; int main() { std::cout << "T t1 : "; T t1; std::cout << "T t2(1) : "; T t2(1); std::cout << "T t3 = 1 : "; T t3 = 1; std::cout << "T t4 = T(1) : "; T t4 = T(1); std::cout << "T t5(1, 2) : "; T t5(1, 2); std::cout << "T t6 = T(1, 2) : "; T t6 = T(1, 2); std::cout << "T t7; t7 = 1 : "; T t7; t7 = 1; std::cout << "T t8; t8 = T(1): "; T t8; t8 = T(1); std::cout << "T t9(t8) : "; T t9(t8); std::cout << "T t10 = 'a' : "; T t10 = 'a'; return 0; }
Компилируем, например в Visual Studio:
cl /EHsc ctor.cpp
и запускаем:
T t1 : T()
T t2(1) : T(int)
T t3 = 1 : T(int)
T t4 = T(1) : T(int)
T t5(1, 2) : T(int, int)
T t6 = T(1, 2) : T(int, int)
T t7; t7 = 1 : T()
T(int)
operator=(const T&)
T t8; t8 = T(1): T()
T(int)
operator=(const T&)
T t9(t8) : T(const T&)
T t10 = 'a' : T(int)
Видно, что во всех этих “разнообразных” способах создания объекта всегда вызывался непосредственно конструктор, а не оператор копирования. Оператор же копирования был вызван только когда знак присваивания использовался явно в отдельном от вызова конструктора операторе. То есть знак “=”, используемый в операторе конструирования объекта так или иначе приводит к вызову конструкторов, а не оператора копирования. И это происходит вне зависимости от какой-либо оптимизации, проводимой компилятором.
Также интересно, как был создана переменная t10
. Видно, что для символьной константы компилятор “подобрал” наиболее подходящий конструктор. Неявным образом был вызвал конструктор от int
. Если подобное поведение не входит в ваши планы, и вам совсем не нужно, чтобы конструктор от int
вызывался, когда идет попытка создать объект от типа, который может быть неявно преобразован в int
, например char
, то можно воспользоваться ключевым словом explicit
:
class T { public: ... explicit T(int) { std::cout << "T(int)" << std::endl; } ... };
Это запретит какое-либо неявное преобразования для аргумента этого конструктора.
Вообще практика объявления любого конструктора с одним параметром со модификатором explicit
является весьма полезной, и позволяет избежать некоторых неприятных сюрпризов, например, если вы хотели вызвать конструктор строки от типа char
, предполагая создать строку, состоящую только из одного символа, а получилось, что этот класс не имеет такого конструктора. Зато есть конструктор от int
, делающий совершенно не то, что вам нужно. Вот и будет сюрприз в виде символьной константы, истолкованной как целое число.
Я обычно по умолчанию пишу explicit
для конструкторов с одним параметром, и очень редко приходится потом убирать этого слово. Тут как со словом const
— сначала можно написать, а потом уже думать нужно ли тут его убрать или нет.
diff
по всем файлам проекта на сервере, но это может занимать минуты при большом проекте.
Например, в Perforce, как и во многих других системах контроля версий это решается выставлением атрибута read-only
по умолчанию на все локальные файлы, находящиеся под контролем версий. Это позволяет исключить “случайное” изменение. Если надо изменить файл, то он помечается как рабочий (в Perforce p4 edit имя_файла
). После этого с файла снимается флаг read-only
, и Perforce добавляет его список “открытых” файлов. Теперь, когда вы хотите узнать, какие файлы у вас сейчас на редактировании, то команда Perforce p4 opened
моментально выдаст список без глобального сканирования изменений. Также p4 diff
столь же мгновенно отобразит сами изменения. По началу, такой подход может напрягать, и я часто слышу жалобы типа проще открыть сразу весь проект по маске через p4 edit ...
, поработать спокойно, а уже под занавес сделать полное сканирование изменений для определений реально измененных файлов и только их отправить на сервер командой p4 submit
.
Я постоянно борюсь с таким подходом, так как чаще всего это приводит к тому, что человек забывает отделить реально измененные файлы от остальных, а Perforce как солдат — ему сказали поместить открытые для редактирование файлы на сервер, он и помещает (может в этом есть какая-то задумка пользователя). И получается, что в набор изменений попадают неизмененные файлы. С точки зрения целостности системы проблем нет, но вот при анализе проблемных изменений начинается кошмар, как надо вручную отсеивать нетронутые файлы. Кроме того, неосторожная работы по маске часто приводит к появлению в репозитории временных файлов, которых не заметили или забыли при записи измененных файлов на сервер. Конечно, их можно вычистить потом, но проще сразу их туда просто не класть.
Perforce предоставляет специальный плагин для Visual Studio, который призван облегчить процесс работы с файлами, находящимися под контролем версий. Вы просто работаете в среде, и когда нужно изменить какой-то файл (например, вы просто начали что-то набивать в окне редактора), то студия сама предложить вам открыть файл для редактирования. Вы скажете “Да”, и файл открывается для работы. В жизни же все обычно отключают этот надоедливый запрос, разрешая по умолчанию открывать файлы без запроса. И мы снова приходим к варианту, когда имеется огромное количество открытых файлов, а изменены только несколько. Поэтому я предпочитаю не ставить этот плагин, а открыть файлы вручную.
Конечно, работая в студии, не всегда удобно постоянно лазить в командную строку для p4 edit ...
. Хочется делать это прямо из меню. Способ есть, и никакой шибко умный плагин не нужен.
Идем в меню Tools
, затем в External tool...
. Далее создаем кнопкой Add
элементы &P4 Edit
(Arguments: edit $(ItemPath)
), &P4 Revert
(Arguments: revert $(ItemPath)
), &P4 Diff
(Arguments: diff $(ItemPath)
) по аналогии с картинкой.
Теперь для открытия за запись файла из текущего окна редактирования надо выбрать в меню Tools->P4 Edit
, для отката изменений — Tools->P4 Revert
, а для просмотра изменений — Tools->P4 Diff
.
Вывод этих команд сохраняется в окне Output
.
По вкусу можно добавить аналогичным образом любую команду из арсенала командного клиента Perforce p4
, но именно эти обычно нужны в 99% случаев. Я обычно назначаю горячие клавиши на эти пункты меню. Для остального можно уже слазить в командную строку или в графический клиент Perforce (P4V
или P4Win
).
Правда, если вам потребуется изменить файл проекта или солюшена, то это придется делать либо в командной строке или в графическом клиенте P4V
.
Лично для меня подобная схема привносит порядок в работу. Я четко знаю, что я изменил, и могу быстренько пробежаться глазами по изменениям перед отправкой их на сервер, отсеив пару-тройку файлов, которые я открыл таки для редактирование, но в этоге не менял.
И под занавес выскажу свое мнение про графический интерфейс для систем контроля версий. Мы используем Perforce. Да, это централизованная система, а не распределенная, как модно сейчас, но для корпоративной разработки так проще. Perforce хорош. Есть шероховатости, но с ними можно жить.
Лично предпочитаю “разговаривать” с Perforce через командную строку, ибо в командной строке можно сделать все, и удобно, когда надо выполнить много рутинных однотипных операций. Но есть один случай, когда именно графический клиент становится настоящим спасением. Это случай слияний изменений и разрешения конфликтов. Проблемы начинаются, когда ты хочешь зафиксировать на сервере свои изменения, а там уже кто-то “потрогал” твои файлы. Это называется конфликт, и его надо разрешать. Делать это в обычном текстовом редакторе, особенно когда конфликтуют сотни пересекающихся строк, практически нереально. Очень медленно, и вероятность ошибки огромна. В Perforce есть удивительная графическая программа для сравнения и слияния изменений. Обычно, если конфликтующие строки не пересекаются, Perforce сам автоматически смешает в правильном порядке. Если же есть пересекающиеся конфликты, то тут уже нужен человек для понимания, что выкинуть, а что оставить.
Утилита слияния в Perforce предоставляет для этого очень удобный сервис. На уровне строк можно выбирать нужные для включения в слияние. В окне одновременно отображаются три исходника: твой текущий, твой базовый, на основе которого ты делал изменения, и текущий из репозитория, с которым и возникает конфликт. А под этим всем внизу отображается результат слияния. Все подсвечивается разными цветами, максимально облегчая выбор правильного варинта. Я даже не знаю, как это может быть еще лучше сделано. Даже изобилующие конфликтами слияния между целыми ветками (например, из рабочей ветки в основную, где уже успели исправить порядочно ошибок) у нас делаются за несколько часов.
Приятно, что графический клиент Perforce P4V
существует не только под Windows (в отличие от старого P4Win
). Он есть под Linux, Solaris, FreeBSD и Mac. Если надо работать сразу под несколькими системам, можно запустить у себя на машине X-сервер, и видеть клиентов со всех платформ одновременно. P4V
использует Qt, посему выглядит почти одинаково на всех системах.
Кстати, выскажу свое субъективное мнение на тему контроля версий. Если бы мне продавали исходники чего-то сложного, то для меня было бы очень важным критерием среди прочих наличие не только вылизанной для продажи “последней супер версии”, но и всей истории разработки, в формате какой-нибудь системы контроля версий и баг-трекинга.
]]>Программа x.c
:
#include <stdio.h> int main() { char s[4]; s[0] = 'C'; s[1] = s[2] = '+'; s[3] = 0; s[sizeof(' ') ^ 5] = 0; printf("%s\n", s); return 0; }
Компилируем и запускаем.
Visual Studio:
cl x.c && x
или в Cygwin:
gcc -o x x.c && x
Имеем следующий результат:
C
А теперь так:
cl /TP x.c && x
или в Cygwin:
g++ -o x x.c && x
Теперь программа печает иное:
C++
Результат повеселил некоторых моих коллег.
Нашел небольшой список еще некоторых “отличий” С и С++, но, пожалуй, этот самый неявный, а значит потенциально опасный.
]]>Вы думаете, такая загрузка будет длиться годы? ну или хотя бы минуты? Нет. На все про все — 25 секунд с хвостиком.
Итак, по порядку.
Все знают QEMU — бесплатная виртуальная машина. Из нее, например, вырос пакет VirtualBox, а KVM унаследовал интерфейс командной строки. Это чистый виртуализатор без всяких там “пара-” приставок. Из-за этого работает небыстро и для критичных по скорости задач слабо применимо, но с другой стороны из-за “чистоты” виртуализации работает на многих платформах и виртуализирует многие платформы, а не только Intel, как большинство “быстрых” виртуальных машин. Из-за все сказанного, QEMU идеален для всякого рода экспериментов и нестандартных задач.
Но мы отвлеклись. Автор QEMU — Fabrice Bellard — написал еще нескольно занимательных программ.
Одной из них является TCC — Tiny C Compiler. Это ультра быстрый и ультра маленький компилятор С. Сразу возникает подозрение — слово “tiny” в название, да еще и “ультра быстрый” и “ультра маленький”. Главный вопрос — какие у него ограничения?
Как заявляет автор, TCC полностью поддерживает стандарт языка С вплоть до ISO C99 включительно, но целевая платформа только x86. Компилятор имеет также мини версию системной библиотеки libc
. Когда это возможно, компилятор совмещает фазы компилирования, ассемблирования и линковки для дополнительного ускорения, хотя поддерживаются стандартные ABI и можно подлинковать что-то готовое.
Компилятор доступен в исходных текстах и в двоичном виде под Windows. Скомпилировать его можно вручную, например, самим же TCC.
Нужно на чем-нибудь проверить TCC, на чем-нибудь нетривиальном. Ядро Linux’а является весьма сложным и большим проектом, это его сборка была бы отличной проверкой.
TCC не только успешно собирает ядро, но и делает это до 9 раз быстрее, чем GCC (естественно, речь идет только о платформе x86).
Невероятная скорость компиляции позволяет использовать TCC как компилирующий “интерпретатор” скриптов. Если добавить первой строкой вашей программы на С строчку #!/usr/local/bin/tcc –run
и установить флаг executable
на исходник, то ваша программа будет запущена в UNIX’е прямо из исходного текста, будучи скомпилированной на лету.
Мы подходим к сути. Автор предлагает вариант загрузки Linux, когда ядро компилируется прямо в процессе загрузки из исходных текстов. Проект называется TCCBOOT. Можно скачать ISO имидж (около 6 мегабайт), записать на болванку, загрузиться с нее и увидеть все самому. Что я и сделал.
Загрузчик ISOLINUX запускает мини образ, в котором содержится TCC, исходники ядра и минимальное окружение для запуска командного интерпретатора под скомпилированным на лету ядром.
Поехали…
Старт, запустился ISOLINUX, началась компиляция ядра:
Все, за 25.4 секунды ядро скомпилировано, запущено, и загружена минимальная UNIX система:
Все, за 25.4 секунды ядро скомпилировано, запущено, и загружена минимальная UNIX система:
Фотографии я делал с рук, так что немного коряво выглядит. Можно было, конечно, все это проделать под виртуальной машиной, тогда бы и скриншоты были бы красивее, но пропало бы ощущение самого главного — чудовищной скорости. Забавно, на первом снимке видно, что строка отображения имен компилируемых файлов смазана — так все “летает”.
Эксперимент проводился на ноутбуке Core 2 1GHz, 2GB RAM.
Я был очень впечатлен. А если в TCC нормально поддержать многопроцессорность? Тут недалеко и до полностью функциональной операционной системы, у которой нет двоичного представления до загрузки.
]]>Но порой нужно таки сделать “а-ля” коробочную установку в стиле “взял один файл-архив, запустил и получил результат”. Ситуация усугубляется, когда все это надо делать под разными сортами UNIXа. Конечно, есть вариант написать этого зверя на Java. На ней можно сделать установщик еще и графическим. Нужно только, чтобы эта Java была у заказчика, то есть опять тема простоты уходит. Нужно что-то простое и легковесное.
Небольшой анализ привел меня к makeself. В двух словах — это саморазархивирующиеся shell-скрипты. То есть вы готовите процедуру установки, сводите все к наличию каталога со всеми необходимыми файлами, и, если нужно, скриптом, которые надо запустить после разархивации. Все как у так называемых SFX (self extract) модулей для WinRAR, например. Прелесть в том, что в итоге вы получаете одиночный файл, который является абсолютно стандартным shell-скриптом, работающим в большом количестве типов UNIX, и который также содержит внутри себя архив с вашими файлами. Все, что нужно сделать на стороне клиента, это запустить этот файл.
Допустим, вы подготовили ваш дистрибутив в каталоге /home/sandbox/intallation
. Также у вас есть скрипт ./setup
, который необходимо запустить после разархивации для локальной настройки, например. Все, что вы делаете:
makeself.sh /home/sandbox/installation megasoft-0.0.1.sh "Mega Software 0.0.1" ./setup
Данная команда создаст файл megasoft-0.0.1.sh
, в который упакуется содержимое каталога /home/sandbox/intallation
и скрипт ./setup
. Теперь все, что надо сделать на стороне клиента, это запустить это файл командой:
. ./megasoft-0.0.1.sh
Скрипт разархивирует собственное содержимое и запустит ваш скрипт setup, который сможет окончательно настроить установку.
makeself
позволяет использовать для компрессии стандартные средства UNIX на выбор — compress, gzip, bzip2. Также содержимое архива дополнительно защищается контрольными суммами: MD5 или CRC. Это может быть полезно, если вы не используете компрессию, а целостность данных проверять все же хотите.
Список же поддерживаемых типов UNIX для текущей версии 2.1 весьма внушителен:
Напомню ссылку на makeself
еще раз — http://megastep.org/makeself/
std::min
и std::max
с одноименными макросами из файла windows.h
мне подсказали интересное решение.
Если вместо, например, std::max(a, b)
написать (std::max)(a, b)
, то результат работы препроцессора выглядит так:
#line 3 "minmax.cpp" int main() { int a = (std::min)(10, 20); return 0; }
вместо:
#line 3 "minmax.cpp" int main() { int a = std::(((10) < (20)) ? (10) : (20)); return 0; }
и конфликта не происходит. Все компилируется без проблем.
Не берусь судить, на сколько это красивое решение, и я бы все-таки предпочел действовать напрямую через макрос NOMINMAX, но выход элегантный.
Другие посты по теме:
]]>Мой класс PreciseTimer
предназначен для работы с миллисекундными интервалами времени. Реализация под Windows основана на использовании функций QueryPerformanceFrequency и QueryPerformanceCounter.
Этот класс активно используется в некоторых наших проектах. Также, в силу некоторых обстоятельств, мы активно используем виртуальные машины для тестовых сборок. И, например, сборка под Windows 64-бита производится под VirtualBox. И вот очередной релиз-кандидат ушел в тестирование. Немедленно мне посыпались жалобы, что сборка не работает под 64-битным Windows под виртуальной машиной.
Я запретил тестерам временно отключать тест и начал проверять все сам. На реальных машинах все работает. Начал гонять на виртуальных. На VMWare тоже глючит. Тест PreciseTimer.MeasurementAccuracy
выдает ошибку типа:
c:\sandbox\test\PreTimer_unittest.cpp(22): error: Value of: delta <= allowed_delta_ms
Actual: false
Expected: true
Delta (100) > than 10
[ FAILED ] PreciseTimer.MeasurementAccuracy (110 ms)
Получается, что задержка в 100 миллисекунд была измерена практически как нулевая.
Я заподозрил функцию QueryPerformanceCounter()
. Написал еще один кондовый тест:
TEST(PreciseTimer, MillisecCounter) { monitor::PreciseTimer timer; monitor::PreciseTimer::Counter a = timer.millisec(); timer.sleepMs(10000); monitor::PreciseTimer::Counter b = timer.millisec(); EXPECT_EQ(10000, b - a); }
Этот тест делает видную глазом задержку в 10 секунд (чтобы исключить проблему в самой задержке) и затем проверят показания таймера.
Итак, на реальной машине тест выдает следующее:
[==========] Running 1 test from 1 test case.
[----------] Global test environment set-up.
[----------] 1 test from PreciseTimer
[ RUN ] PreciseTimer.MillisecCounter
c:\sandbox\test\PreTimer_unittest.cpp(17): error: Value of: b - a
Actual: 9995
Expected: 10000
[ FAILED ] PreciseTimer.MillisecCounter
[----------] Global test environment tear-down
[==========] 1 test from 1 test case ran.
[ PASSED ] 0 tests.
[ FAILED ] 1 test, listed below:
[ FAILED ] PreciseTimer.MillisecCounter
1 FAILED TEST
Тест, конечно, сбоит, но тут четко видно, что требуемая задержка в 10000 миллисекунд (10 секунд) измерена как 9995 миллисекунд. Понятно, тут невозможно измерить точь в точь, но суть работает верно.
А вот, что я получил на виртуальное машине:
[==========] Running 1 test from 1 test case.
[----------] Global test environment set-up.
[----------] 1 test from PreciseTimer
[ RUN ] PreciseTimer.MillisecCounter
c:\sandbox\test\PreTimer_unittest.cpp(17): error: Value of: b - a
Actual: 90
Expected: 10000
[ FAILED ] PreciseTimer.MillisecCounter
[----------] Global test environment tear-down
[==========] 1 test from 1 test case ran.
[ PASSED ] 0 tests.
[ FAILED ] 1 test, listed below:
[ FAILED ] PreciseTimer.MillisecCounter
1 FAILED TEST
Задержка в 10000 миллисекунд была измерена всего как 90. Вот и причина сбоя — функция QueryPerformanceCounter()
. Полчаса работы.
Затем я поискал в интернете на тему проблем у функции QueryPerformanceCounter()
на виртуальных машинах и нашел объяснение в MSDN. Корень проблемы, как оказалось, был не конкретно в виртуальных машинах, а в “старом” биосе и в использовании мультиядерных систем. На наших реальных мультиядерных машинах все работало, так как, видимо их биос был “нормальным”.
В итоге проблема решилась добавлением параметра /usepmtimer
в файл c:\boot.ini
, как рекомендуется в найденной статье. После этого все тесты заработали как положено.
Я включил подробное описание проблемы в Release Notes, чтобы клиенты не наступили на эти грабли, и инцидент был исчерпан. Еще полчаса работы. Итого час на все.
А теперь вдумайтесь в произошедшее. Проблема была локализована и исправлена не то, чтобы до релиза. Она была локализована даже до тестового запуска. Лично я вот ну ни как не ожидал, что в Windows функция QueryPerformanceCounter()
почему-то как-то особенно работает на мультиядерных системах со “старым” биосом (видимо биосы VMWare и VirtualBox как раз подходят под эту категорию). А вот как бы искал эту проблему потом? уже на реальной работающей системе. Одно из применений этого класса у нас, это измерения временных данных по транзакциям. Да я потратил был потом полжизни для поиска этой “маленькой проблемки”, случись она у реального клиента.
Пишите тесты! Это экономит не только деньги, но самое драгоценное — ваши нервы.
Другие посты по теме:
]]>Мифический человеко-месяц или как создаются программные системы
Как многие программисты, я весьма нетерпелив, и люблю как можно быстрее переходить к делу, а лучше — к реальным и осязаемым результатам. Поэтому с большим трудом читаю книги по профессии, где уж слишком много словоблудия, и особенно осторожен, когда кто-то пытается чего-то там теоретизировать. Я глубоко уверен, что кумиров “по умолчанию” в профессии иметь опасно, а лучше вообще не иметь, и любую веру в “крутизну” кого-либо надо проверять лично, поэтому я распечатал себе эту книгу (у меня получилось около 70 листов А4 с двух сторон), и решил полистать вечерком у телевизора.
В итоге, я не отрываясь внимательно прочитал ее до конца часа за два, и некоторые куски потом пересматривал. По моему мнению, это надо прочесть любому программисту или руководителю программистов. Несмотря на то, что книге скоро стукнет 35 лет, и ее переиздание 1995 года практически повторяет оригинальное, в новом издании всего добавили пару глав, но старые главы остались в исходном виде. Даже когда автор употребляет несколько угловатые в наши дни выражения типа “выйти на машину” или “обратиться к журналу с дисплейного терминала” — это совершенно не искажает сути. Видимо из-за собственной самоуверенности, я считал, что такие значимые для меня вещи как многоуровневое тестирование, самодокументируемый код, контроль версий, готовность вносить изменения и т.д. придуманы совершенно недавно, можно сказать, на моих глазах. “Ах какой удар от классика!” — все это придумано и применялось уже тогда.
Лично я получил отличную пищу для ума, начиная от обдумывания аналогий о том, как делали что-то раньше, и во что это трансформировалось сейчас, и заканчивая абсолютно точным описанием “смоляной ямы” (программирование больших и сложных систем), в которой могут пропасть самые выдающиеся умы, и как в нее по возможности не попасть. Если бы все начальники читали бы эту книгу, они бы знали, что если проект по написанию программного обеспечения прогибается по срокам, то “влив” в него еще людей может только ускорить его кончину.
Вывод
Два часа прочтения будут отзываться в вас еще долго, а необычные сегодня картины вычислительной техники “тех” дней только помогут кристаллизовать абсолютно актуальные до сих пор выводы Фредерика Брукса.
Посты по теме:
]]>Мир языка С++ не такой дружественный к тестированию, как например, мир Java, C# или мир интерпретаторов. Главная причина — крайне слабый механизм интроспекции, то есть возможности исследования двоичного кода в плане получения информации о структуре исходных текстов. В Java, например, есть Reflection
, с помощью которого можно прямо на основе скомпилированных классов создать тестовую среду (понять иерархию классов, типа аргументов и т.д.). В С++ приходится многое закладывать в исходный текст на этапе его создания, чтобы облегчить будущее тестирование.
А что же мы имеем в С? Тут, как мне кажется, разрыв в удобстве тестирования по отношению к С++ в разы больше, чем между С++ и Java, например. Причин море: процедурная модель вместо объектно-ориентированной, отсутствие интроспекции вообще, крайне слабая защита при работе с памятью и т.д.
Но шансы все же остались. Я начал поиск готовых библиотек для unit-тестирования в С. Например, есть библиотека MinUnit, длиной в четыре строки. Вполне жизненно. Следующий вполне себе вариант — это CUnit. Тут даже есть продвинутый консольный интерфейс.
Перебрав еще несколько вариантов, я остановился на гугловской библиотеке cmockery. Мне понравилось, что библиотека, несмотря на весьма сложный код, успешно компилируются не только в Visual Studio и GNU C, но и “родными” компиляторами AIX, HP-UX, SunOS и некоторых других экзотических зверей. Также библиотека умеет отлавливать утечки памяти, неправильную работу с распределенными кусками памяти (так называемые buffer over- и under- run). Еще в cmockery
есть зачатки mock-механизмов, то есть когда задаются предполагаемые сценарии выполнения тестируемого блока, и потом результаты тестового прогона сверяются с предполагаемым сценарием. Mock-возможности я не буду пока рассматривать в данной статье. Про это стоит написать отдельно.
На текущий момент актуальной версией cmockery
является 0.1.2. Из всего архива реально нужны только два файла: cmockery.c
и cmockery.h
. Можно, конечно, собрать библиотеку как положено, в двоичном виде, но я предпочитаю работать всегда с исходными текстами, благо компилируется очень быстро (это ж не С++).
Желающие, могут скачать мою сборку cmockery. В этом архиве только необходимые два файла cmockery.c
и cmockery.h
. Также в файл cmockery.h
я внес небольшое изменение, связанное к тем, что функция IsDebuggerPresent()
почему-то явно объявлена в заголовочных файлах только в Visual Studio 2008. Для студии 2003 и 2005 надо вручную объявлять прототип, иначе при линковке вылезает сообщение:
error LNK2019: unresolved external symbol _IsDebuggerPresent referenced in function __run_test
Я отрапортовал об этом досадном недочете авторам, и пока новый релиз cmockery не вышел, можно пользоваться моей сборкой, которая без предупреждений компилируются в любой студии.
Теперь пример реального использования cmockery
.
Я долго выбирал то, на чем можно хоть как-то наглядно продемонстрировать unit-тестирование в С. В итоге я остановился на библиотеке для работы со строками. Эта библиотека реализует так называемые строки с длинной. То есть надо для кода на С дать более менее удобный интерфейс для манипулированию строками, которые хранят внутри себя длину.
Основа библиотеки была написана весьма давно, и много раз переписывалась практически с нуля, но я все еще использую ее в некоторых проектах.
Естественно, я не буду приводить всю библиотеку. Во-первых, она весьма тривиальна и вся ее “фишка” состоит в удобности работы, нежели в какой-то особо хитрой и заумной реализации. Во-вторых, полный ее исходный текст весьма объемен. Я выбрал небольшой ее фрагмент, но его тестирование позволяет почувствовать дух тестирования в С.
Итак, библиотека cstring
. Тут можно создавать в некоторые “объекты”, реализованные через структуры, которые представляют собой “строки”. Такая “строка” может создаваться либо в стеке (автоматическая переменная), либо в куче. Также предоставляется набор разнообразных базовых функций: определение длины, копирование, склейка, интерфейс со строками языка С (char *)
и т.д. Как я уже сказал, для демонстрации системы тестирования я оставил только несколько функций.
Заголовочный файл cstring.h
:
#ifndef _CSTRING_H #define _CSTRING_H #define _decl_string_t(N) \ struct { \ int sz; \ char data[N]; \ } typedef _decl_string_t(1) string_t; /** * Объявление строки в форме автоматической переменной в стеке. * Длина строки инициализируется нулем. */ #define decl_string_t(name, size) _decl_string_t(size) name = { 0 } /** * Создание новой строки в куче. */ string_t* string_new(int sz); /* Трюк с дублированием имен функций, начинающихся с символа '_' * требуется для подавление предупреждений компилятора о преобразовании * типов. */ /** * Удаление строки из кучи. */ #define string_delete(str) _string_delete((string_t*)str) void _string_delete(string_t* str); /** * Текущая длина строки. */ #define string_length(str) _string_length((const string_t*)str) int _string_length(const string_t* str); /** * Изменение длины строки. */ #define string_resize(str, sz) _string_resize((string_t*)str, sz) int _string_resize(string_t* str, int sz); /** * Копирование строки из строки С, завершающейся нулем. */ #define string_from_c_str(dst, src) _string_from_c_str((string_t*)dst, src) string_t* _string_from_c_str(string_t* dst, const char* src); /** * Добавление символа в строку. */ #define string_append_ch(str, ch) _string_append_ch((string_t*)str, ch) string_t* _string_append_ch(string_t* str, char ch); /** * Превращение строки в строку С без добавления нуля на конце. */ #define string_data(str) str->data /** * Превращение строки в строку С с нулем на конце. */ #define string_c_str(str) _string_c_str((string_t*)str) char* _string_c_str(string_t* str); #endif
Файл cstring.c
:
#include <stdlib.h> #include "cstring.h" /** * Подготовительная площадка для тестирования. * Если задан макрос UNIT_TESTING, то функции работы с кучей подменяются * на тестовые. */ #if UNIT_TESTING extern void* _test_malloc(const size_t size, const char* file, const int line); extern void* _test_calloc(const size_t number_of_elements, const size_t size, const char* file, const int line); extern void _test_free(void* const ptr, const char* file, const int line); #define malloc(size) _test_malloc(size, __FILE__, __LINE__) #define calloc(num, size) _test_calloc(num, size, __FILE__, __LINE__) #define free(ptr) _test_free(ptr, __FILE__, __LINE__) #endif // UNIT_TESTING /** * Создание новой строки в куче. Трюк "sizeof(string_t)" используется, чтобы * правильно отработать ситуацию, если из-за выравнивания между элементами * структуры string_t 'sz' и 'data' вдруг появится промежуток. */ string_t* string_new(int sz) { return malloc(sizeof(string_t) + sz - 1); } /** * Удаление строки из кучи. */ void _string_delete(string_t* str) { free((void *)str); } /** * Текущая длина строки. */ int _string_length(const string_t* str) { return str->sz; } /** * Изменение длины строки. */ int _string_resize(string_t* str, int sz) { return str->sz = sz; } /** * Копирование строки из строки С, завершающейся нулем. */ string_t* _string_from_c_str(string_t* dst, const char* src) { int sz = strlen(src); memcpy(dst->data, src, sz); dst->sz = sz; return dst; } /** * Добавление символа в строку. */ string_t* _string_append_ch(string_t* str, char ch) { str->data[str->sz++] = ch; return str; } /** * Превращение строки в строку С с нулем на конце. Фактически, * в тело строки добавляется ноль и возвращается указатель на данные. */ char* _string_c_str(string_t* str) { str->data[str->sz] = 0; return string_data(str); }
Как вы заметили, в коде есть специальный блок, ограниченный макросом UNIT_TESTING
. Ничего не поделаешь, в языке С приходится “готовить” код к потенциальному тестированию и вставлять фрагменты, позволяющие тестовой среде работать с этим кодом. Этот блок, если задан макрос UNIT_TESTING
, переопределяет функции работы с кучей, чтобы можно было перехватывать их вызовы. Подменяющие функции _test_malloc()
, _test_calloc()
и _test_free()
предоставляются библиотекой cmockery
.
Теперь файл тестов cstring_unittest.c
:
#include <stdarg.h> #include <stddef.h> #include <setjmp.h> #include <cmockery.h> #include "cstring.h" /** * Тестируем декларацию строки длиной 20 в виде автоматической * переменной, добавляем в нее два символа, обрезаем строку * до длины в один байт и проверяем, добавился ли 0 при преобразовании * в строку С. */ void string_c_str_test(void **state) { decl_string_t(a, 20); a.data[0] = 'a'; a.data[1] = 'b'; a.sz = 1; assert_memory_equal("a\0", string_c_str(&a), 2); } /** * Тестируем изменение длины строки. */ void string_resize_test(void **state) { decl_string_t(a, 20); a.sz = 2; string_resize(&a, 1); assert_int_equal(1, string_length(&a)); } /** * Тестируем добавление символа путем сравнения со строками С */ void string_append_ch_test(void **state) { decl_string_t(a, 20); assert_string_equal("", string_c_str(&a)); assert_string_equal("a", string_c_str(string_append_ch(&a, 'a'))); assert_string_equal("ab", string_c_str(string_append_ch(&a, 'b'))); } /** * Тестируем декларацию строки в виде автоматической переменной. * Длина строки сразу после декларации должна быть нулевой. */ void string_declare_test(void **state) { decl_string_t(a, 20); assert_int_equal(0, string_length(&a)); } /** * Тестируем размещение новой строки в куче и ее удаление из нее. */ void string_heap_allocation_test(void **state) { string_t* a = string_new(20); string_delete(a); } /** * Тестируем копирование строки из строки С с нулем на конце. */ void string_from_c_str_test(void **state) { string_t* a = string_new(8); string_from_c_str(a, "12345678"); assert_int_equal(8, string_length(a)); string_delete(a); } /** * Создаем список тестов и запускаем их. */ int main(int argc, char* argv[]) { const UnitTest tests[] = { unit_test(string_declare_test), unit_test(string_c_str_test), unit_test(string_append_ch_test), unit_test(string_heap_allocation_test), unit_test(string_from_c_str_test), unit_test(string_resize_test), }; return run_tests(tests); }
Схема очень похожа на любое другое xUnit тестирование: каждый тест проверяет какой-то один функциональный элемент, тесты объединяются в группы и запускаются автоматически все вместе. Правда, из-за ограничений языка С каждый тест приходится вручную добавлять в список запуска, увы.
Как я уже сказал, для компиляции потребуются файлы cmockery.c
и cmockery.h
(см. выше). Эти файлы можно положить в текущий каталог.
Компилируем в Visual Studio:
cl /DUNIT_TESTING /I. cstring_unittest.c cstring.c cmockery.c
Если все скомпилировалось нормально, то запускаем файл cstring_unittest
:
string_declare_test: Starting test
string_declare_test: Test completed successfully.
string_c_str_test: Starting test
string_c_str_test: Test completed successfully.
string_append_ch_test: Starting test
string_append_ch_test: Test completed successfully.
string_heap_allocation_test: Starting test
string_heap_allocation_test: Test completed successfully.
string_from_c_str_test: Starting test
string_from_c_str_test: Test completed successfully.
string_resize_test: Starting test
string_resize_test: Test completed successfully.
All 6 tests passed
Все тесты отработали правильно.
Но неинтересно, когда все работает. Внесем в тест библиотеки “случайные ошибки”. Каждую из них можно спокойно допустить непреднамеренно. Строки с ошибками я пометил комментариями со словом “ОШИБКА (!)”. Посмотрим, как cmockery
справится с этим.
Файл cstring.c
с “ошибками”:
#include <stdlib.h> #include "cstring.h" /** * Подготовительная площадка для тестирования. * Если задан макрос UNIT_TESTING, то функции работы с кучей подменяются * на тестовые. */ #if UNIT_TESTING extern void* _test_malloc(const size_t size, const char* file, const int line); extern void* _test_calloc(const size_t number_of_elements, const size_t size, const char* file, const int line); extern void _test_free(void* const ptr, const char* file, const int line); #define malloc(size) _test_malloc(size, __FILE__, __LINE__) #define calloc(num, size) _test_calloc(num, size, __FILE__, __LINE__) #define free(ptr) _test_free(ptr, __FILE__, __LINE__) #endif // UNIT_TESTING /** * Создание новой строки в куче. Трюк "sizeof(string_t)" используется, чтобы * правильно отработать ситуацию, если из-за выравнивания между элементами * структуры string_t 'sz' и 'data' вдруг появится промежуток. */ string_t* string_new(int sz) { return malloc(sizeof(string_t) + 1 - 1); // (ОШИБКА!) "Неверная" длина. } /** * Удаление строки из кучи. */ void _string_delete(string_t* str) { // (ОШИБКА!) "Забыли" вызвать free(). } /** * Текущая длина строки. */ int _string_length(const string_t* str) { return str->sz; } /** * Изменение длины строки. */ int _string_resize(string_t* str, int sz) { return str->sz; // (ОШИБКА!) "Забыли" уменьшить длину строки. } /** * Копирование строки из строки С, завершающейся нулем. */ string_t* _string_from_c_str(string_t* dst, const char* src) { int sz = strlen(src); memcpy(dst->data, src, sz); // (ОШИБКА!) "Забыли" присвоить длине новое значение. return dst; } /** * Добавление символа в строку. */ string_t* _string_append_ch(string_t* str, char ch) { str->data[str->sz] = ch; // (ОШИБКА!) "Забыли" увеличить длину. return str; } /** * Превращение строки в строку С с нулем на конце. Фактически, * в тело строки добавляется ноль и возвращается указатель на данные. */ char* _string_c_str(string_t* str) { // (ОШИБКА!) "Забыли" добавить 0 в конец. return string_data(str); }
Компилируем и запускаем:
string_declare_test: Starting test
string_declare_test: Test completed successfully.
string_c_str_test: Starting test
difference at offset 1 0x00 0x62
1 bytes of 0x0040f014 and 0x0012fe7c differ
ERROR: cstring_unittest.c:19 Failure!
string_c_str_test: Test failed.
string_append_ch_test: Starting test
"ab" != "b"
ERROR: cstring_unittest.c:39 Failure!
string_append_ch_test: Test failed.
string_heap_allocation_test: Starting test
Blocks allocated...
0x00326ee0 : cstring.c:27
ERROR: string_heap_allocation_test leaked 1 block(s)
string_heap_allocation_test: Test failed.
string_from_c_str_test: Starting test
Blocks allocated...
0x00326ee0 : cstring.c:27
Guard block of 0x00326f18 size=8 allocated by cstring.c:27 at 0x00326f20 is corrupt
ERROR: cmockery.c:1379 Failure!
string_from_c_str_test: Test failed.
string_resize_test: Starting test
0x1 != 0x2
ERROR: cstring_unittest.c:29 Failure!
string_resize_test: Test failed.
5 out of 6 tests failed!
string_c_str_test
string_append_ch_test
string_heap_allocation_test
string_from_c_str_test
string_resize_test
Blocks allocated...
0x00326ee0 : cstring.c:27
Guard block of 0x00326f18 size=8 allocated by cstring.c:27 at 0x00326f20 is corrupt
ERROR: cmockery.c:1379 Failure!
Бам! 5 из 6 тестов сломаны. Проанализируем полученное.
Тест string_c_str_test
выявил, что функция string_c_str
не добавила 0 в конец строки, хотя должна была:
string_c_str_test: Starting test
difference at offset 1 0x00 0x62
1 bytes of 0x0040f014 and 0x0012fe7c differ
ERROR: cstring_unittest.c:19 Failure!
string_c_str_test: Test failed.
Тест string_append_ch_test
выявил, что функция добавления символа в конец строки не работает:
string_append_ch_test: Starting test
"ab" != "b"
ERROR: cstring_unittest.c:39 Failure!
string_append_ch_test: Test failed.
Тест string_heap_allocation_test
выявил, что у нас имеется неосвобожденный блок памяти (утечка?). Конечно, мы же “забыли” освободить память в функции string_delete()
:
string_heap_allocation_test: Starting test
Blocks allocated...
0x00326ee0 : cstring.c:27
ERROR: string_heap_allocation_test leaked 1 block(s)
string_heap_allocation_test: Test failed.
Тест string_from_c_str_test
выявил, что мы “вылезли” за границы выделенного куска памяти. Мы записали что-то мимо. Это болезненная ошибка. Конечно, cmockery
не всегда может находить такие ляпы. Например, если переменная выделена с стеке, а не в куче, то проблема не вскроется. Тут уже помогут только динамические отладчики типа valgrind:
string_from_c_str_test: Starting test
Blocks allocated...
0x00326ee0 : cstring.c:27
Guard block of 0x00326f18 size=8 allocated by cstring.c:27 at 0x00326f20 is corrupt
ERROR: cmockery.c:1379 Failure!
string_from_c_str_test: Test failed.
Тест string_resize_test
показал, что функция изменения размера строки не работает как положено:
string_resize_test: Starting test
0x1 != 0x2
ERROR: cstring_unittest.c:29 Failure!
string_resize_test: Test failed.
В целом, очень неплохие результаты.
Теперь представьте, что вы решили переписать реализацию библиотеки под новый процессор, чтобы работало в десять раз быстрее. Но как проверить результат? Элементарно. Запустите старые тесты. Если они работают, то по крайней мере с большой вероятностью вы не сломали старую функциональность. И, кстати, чем более тщательно написаны тесты, тем более ценны они. Чем более критична какая часть системы для стабильности системы в целом (например, библиотека строк или каких-то базовых контейнеров), тем более тщательно они должны быть покрыты тестами.
Конечно, уровень комфорта при написании тестов на С и их отладке очень далек даже от С++, но это не может быть оправданием для отказа от тестирования. Честно могу сказать, часто результатом работы “сломанного” теста в С, который неверно работает с памятью, например, может является просто зависание, а не красивый отчет, что тест “не работает”. Но даже такой “знак” очень важен и дает понять, что что-то сломано. Пусть лучше повиснет тест, нежели готовый продукт у заказчика.
Под занавес приведу список основных функций-проверок (assert
-фукнции), которые доступны в cmockery
:
assert_true()
, assert_false()
— проверка булевых флаговassert_int_equal()
, assert_int_not_equal()
— сравнение для типа int
assert_string_equal()
, assert_string_not_equal()
— сравнение для типа char*
(для С-строк, заканчивающихся нулем)assert_memory_equal()
, assert_memory_not_equal()
— сравнение кусков памятиassert_in_range()
, assert_not_in_range()
— проверка нахождения числа в указанном интервалеassert_in_set()
, assert_not_in_set()
— проверка нахождения строки (char*)
среди заданного набора строкfail()
— безусловное завершения теста с ошибкойВывод
Unit-тестирование в С порой сопряжено с трудностями, но оно возможно. И нет причин от него отказываться.
]]>Модифицированный текст файла coredump.cpp
, в котором с помощью макроса UNIT_TESTING
встроена поддержка для тестирования. Если этот макрос определен, то, как я уже сказал, подавляется появление окна с ошибкой, и coredump
файл создается с постоянным именем.
Файл coredump.cpp
:
#include <windows.h> #include <dbghelp.h> #include <stdio.h> // _snprintf // Наш обработчик непойманного исключения. static LONG WINAPI ExceptionFilter(EXCEPTION_POINTERS* ExceptionInfo); // Статический экземпляр переменной, конструктор которой // вызывается до начала функции main(). static struct CoredumpInitializer { CoredumpInitializer() { SetUnhandledExceptionFilter(&ExceptionFilter); } } coredumpInitializer; LONG WINAPI ExceptionFilter(EXCEPTION_POINTERS* ExceptionInfo) { char fname[_MAX_PATH]; SYSTEMTIME st; GetLocalTime(&st); HANDLE proc = GetCurrentProcess(); #ifdef UNIT_TESTING lstrcpy(fname, "___coredump.dmp"); #else // Формируем имя для coredump'а. _snprintf( fname, _MAX_PATH, "coredump-%ld-%ld-%04d%02d%02d%02d%02d%02d%03d.dmp", GetProcessId(proc), GetCurrentThreadId(), st.wYear, st.wMonth, st.wDay, st.wHour, st.wMinute, st.wSecond, st.wMilliseconds ); #endif // Открываем файл. HANDLE file = CreateFile( fname, GENERIC_READ|GENERIC_WRITE, FILE_SHARE_READ, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL ); MINIDUMP_EXCEPTION_INFORMATION info; info.ExceptionPointers = ExceptionInfo; info.ThreadId = GetCurrentThreadId(); info.ClientPointers = NULL; // Собственно, сбрасываем образ памяти в файл. MiniDumpWriteDump( proc, GetProcessId(proc), file, MiniDumpWithFullMemory, ExceptionInfo ? &info : NULL, NULL, NULL ); CloseHandle(file); #ifdef UNIT_TESTING return EXCEPTION_EXECUTE_HANDLER; #else return EXCEPTION_CONTINUE_SEARCH; #endif }
Теперь, собственно, тест:
Файл coredump_unittest.cpp
:
#include "gtest/gtest.h" #include <fstream> #include <windows.h> #include <stdlib.h> TEST(Coredump, CoredumpCreation) { const char* coredump = "___coredump.dmp"; // На всякий случай заведомо стираем старые файлы. EXPECT_EQ(0, std::system("cmd.exe /c del ___coredump_main.* 1>nul 2>&1")); // Создаем файл с тестовой программой. std::string program = "int main() { *(char *)0 = 0; return 0; }"; std::ofstream os("___coredump_main.cpp"); os << program << std::endl; os.close(); // Компилируем тестовую программу с опцией UNIT_TESTING. // С этой опцией coredump файл будет создаваться с постоянным // именем "___coredump.dmp", и будет подавляется окно с сообщением // об ошибке. EXPECT_EQ( 0, std::system( "cl /Zi /DUNIT_TESTING /Fe___coredump_main.exe" " ___coredump_main.cpp coredump.cpp dbghelp.lib" " 1>nul 2>&1" ) ); // На всякий случая удаляем старый coredump файл. std::remove(coredump); // Убеждаемся, что файл действительно удалился. std::ifstream isdel(coredump); EXPECT_FALSE(isdel.good()); isdel.close(); // Запускаем тестовую программу. ASSERT_EQ(0xC0000005, std::system("___coredump_main.exe")); // Проверяем, создался ли файл coredump.dmp. std::ifstream is(coredump); EXPECT_TRUE(is.good()); is.close(); // Удаляем за собой временные файлы. EXPECT_EQ(0, std::system("cmd.exe /c del ___coredump_main.* 1>nul 2>&1")); std::remove(coredump); }
Данный тест имеет ряд существенных недостатков. Во-первых, он использует файловую систему, и во-вторых, он вызывает компилятор, что занимает небольшое, но все же время. Недостатки неприятные, но в целом приемлемые.
Кстати, Google Test Framework умеет делать так называемые “смертельные” (death) тесты. То есть можно протестировать именно аварийное “падение” фрагмента кода, например, из-за нарушения защиты памяти, и для проведения такого теста не надо вручную компилировать что-либо, как мы делали тут. К сожалению, эта возможность основана на использования юниксового системного вызова fork()
и поэтому доступна только на UNIX платформах.
Дежурный файл для запуска тестов (runner.cpp
):
#include "gtest/gtest.h" int main(int argc, char* argv[]) { testing::InitGoogleTest(&argc, argv); return RUN_ALL_TESTS(); }
Традиционно, для компиляции тестов нам нужна Google Test Framework. Как я уже писал, вы можете скачать мою модификацию этой библиотеки, которая сокращена до двух необходимых файлов gtest/gtest.h
и gtest-all.cc
.
Компилируем, например в Visual Studio 2008:
cl /EHsc /I. /Fecoredump_unittest_vs2008.exe /DWIN32 runner.cpp coredump_unittest.cpp gtest\gtest-all.cc`
Запускаем:
[==========] Running 1 test from 1 test case.
[----------] Global test environment set-up.
[----------] 1 test from Coredump
[ RUN ] Coredump.CoredumpCreation
[ OK ] Coredump.CoredumpCreation
[----------] Global test environment tear-down
[==========] 1 test from 1 test case ran.
[ PASSED ] 1 test.
Работает.
Сразу скажу, я проверял все это только под Windows XP SP2 и Server 2003. Пожалуйста, сообщайте, если есть какие-то проблемы или тонкости под другими виндами.
Как это часто бывает в unit-тестировании, тест получился больше, чем сам тестируемый код. Но повторюсь — это того стоит. Буквально скоро расскажу о моих приключениях с модулем таймера, и как меня выручили тесты.
Другие посты по теме:
]]>Книга содержит 27 “этюдов”. Каждый этюд – это законченная задача для обучающихся программированию. Удивительно, книге более 30 лет, но любой из этюдов может быть до сих пор использован по назначению. Я сам с удовольствием давал этюды из этой книги студентам.
Тематика задач совершенно разная: ретроспективная задача по программе, которая печатает свой текст, игра Джона Конвея “Жизнь”, игровое и имитационное моделирование, интерпретаторы форматов и форматирование текста, статистический анализ карточных игр, символьные алгебраические вычисления, плавающая арифметика и работа с числами большой разрядности, математические методы взлома шифров, искусственный интеллект, машина Тьюринга, рекурсивные алгоритмы поиска, компрессия данных, эмуляция виртуальной компьютерной архитектуры, связывающий загрузчик, компилятор, интерпретатор символьного а-ля функционального языка и т.д. И все эти задачи времен, когда программировали на фортране с помощью перфокарт!
Удивительно, как человек смог написать столь долгоживущую книгу, легкого и неперегруженного чрезмерной научностью содержания. Я пытался найти какие-то еще книги Уэзерелла, но безуспешно. Может это его единственная книга.
И я бережно храню переведенный на русский экземпляр до сих пор. Не исключено, что эта книга пришлась мне в нужное время, и поэтому она стала моей главной книгой.
С различным успехом, но с огромным азартом, я возился в разные времена со всеми этюдами.
Был интересный эпизод с одним из этюдов. В главе 24, под названием “Секрет фирмы, или Математический подход к раскрытию шифров” описывается задача по взлому шифра Виженера (точнее, одной из его модификаций). В книге давался зашифрованный текст, описывался метод взлома и предлагалось расшифровать секретное послание.
И так меня эта задача торкнула, что я даже списался с одним из технических переводчиков этой книги. Тогда еще мне пришлось делать это по обычной бумажной почте России. У меня была масса вопросов, так как расшифровать предлагаемый в книге пример не получалось (математический анализ я тогда еще не изучал). Мне ответили, и среди всего прочего рассказали, как они сами (те, кто переводил книгу) расшифровывали. Ведь им, чтобы опубликовать это на русском языке, надо было иметь исходное сообщение, но в книге не приводился ответ, так что пришлось засучить рукава и заняться английской шифровкой:
Решить задачу предложенным автором способом не получилось, и наши решили все по-своему (советская математическая школа показала себя), выявив, что метод автора не совсем правильный. В русском варианте в главе 24 есть “партия переводчика”, где описывается “советский” способ решения. Попутно наши переводчики выяснили, что в английском оригинале, “Etudes for programmers”, есть опечатка! Одна из строк шифровки просто пропущена. Уже после перевода они связывались с господином Уэзереллом, и тот подтвердил факт наличия досадной опечатки.
Кстати, в интернете ходит много решений “этюдов” Уэзерелла. Например, программа, играющая в Калах, или интерпретатор символьного интерактивного языка TRAC.
Но теперь к самому главному.
Пару дней назад королевская почта доставила мне то, что я так давно мечтал полистать лично — это английский оригинал этой книги.
Книга была издана аж в 1978 (я тогда даже не ходил пешком под стол, а просто лежал) и более не переиздавалась. Я купил списанный библиотечный экземпляр. Немного потертый, переклеенный скотчем по корешку, с библиотечным кармашком для карточки на обратной стороне обложки. Экстаз, один словом.
Если в мире программирования могут существовать иконы, то “Этюды для программистов” Чальза Уэзеррела одна из них.
]]>Файл main.cpp
:
int main() { *(char *)0 = 0; return 0; }
В UNIX’е самым вероятным исходом будет сообщение:
Segmentation fault
Это означает безусловное падение из-за нарушения защиты памяти. Если пользователь разрешил создание coredump
файлов (например, для командного интерпретатора bash
это делается командой):
ulimit -c unlimited
то после запуска будет создан файл coredump
. Этот файл фактически содержит образ памяти вашей программы в момент возникновения ошибки, и его можно открыть в отладчике, например в gdb
:
gdb -c coredump
После чего командой bt
(back trace) можно точно установить место в программе, в котором произошла ошибка. Естественно, для удобства, программа должна быть скомпилирована с включенной отладочной информацией.
Когда ошибка происходит в процессе отладки у вас на глазах, то проще запустить программу сразу под отладчиком. Но когда, например, ошибка происходит у заказчика, и вас нет рядом, то тогда можно попросить прислать вам coredump
файл для анализа. Во многих случаях этого хватает для локализации проблемы.
А что делать под Windows? Запуск приведенной программы под виндами обычно приведет вот к такому сообщению:
Неискушенный пользователь обычно жмет Don't send
, и затем программа благополучно падает, и не останется никакой информации произошедшей ошибке. Конечно, программа может вести подробное журналирование, но часто ошибки подобного рода редко удается локализовать по журналам.
Я слышал, что может так случиться, что после нажатия Send Error Report
Майкрософт с вами свяжется и поможет решить проблему. Со мной такого ни разу не случалось, увы.
В Windows тоже есть схожий механизм создания “на лету” образов памяти работающего процесса, но для его использования надо немного потрудиться.
Windows предоставляет механизм исключений, чем-то схожий с исключениями в С++ и системными сигналами в UNIX. На С++ эти исключения похожи try-catch
синтаксисом, а на UNIX — тем, что можно перехватывать исключительные ситуации (например, ошибки) в программе типа нашей (для UNIX можно было бы перехватить сигнал SIGSEGV
и получить управление при возникновении подобной ошибки).
Естественно, сигналы в UNIX используются не только для этого.
Итак, ниже я приведу исходный текст небольшого модуля, который будет создавать аналогичный по смыслу юниксовому coredump’у файл, по которому можно будет установить причину аварийного завершения программы. Все, что нужно — это прилинковать этот модуль в ваш проект. Ничего вызывать специально не надо. Модуль инициализируется автоматически.
Файл coredump.cpp
:
#include <windows.h> #include <dbghelp.h> #include <stdio.h> // _snprintf // Наш обработчик непойманного исключения. static LONG WINAPI ExceptionFilter(EXCEPTION_POINTERS* ExceptionInfo); // Статический экземпляр переменной, конструктор которой // вызывается до начала функции main(). static struct CoredumpInitializer { CoredumpInitializer() { SetUnhandledExceptionFilter(&ExceptionFilter); } } coredumpInitializer; LONG WINAPI ExceptionFilter(EXCEPTION_POINTERS* ExceptionInfo) { char fname[_MAX_PATH]; SYSTEMTIME st; GetLocalTime(&st); HANDLE proc = GetCurrentProcess(); // Формируем имя для coredump'а. _snprintf( fname, _MAX_PATH, "coredump-%ld-%ld-%04d%02d%02d%02d%02d%02d%03d.dmp", GetProcessId(proc), GetCurrentThreadId(), st.wYear, st.wMonth, st.wDay, st.wHour, st.wMinute, st.wSecond, st.wMilliseconds ); // Открываем файл. HANDLE file = CreateFile( fname, GENERIC_READ|GENERIC_WRITE, FILE_SHARE_READ, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL ); MINIDUMP_EXCEPTION_INFORMATION info; info.ExceptionPointers = ExceptionInfo; info.ThreadId = GetCurrentThreadId(); info.ClientPointers = NULL; // Собственно, сбрасываем образ памяти в файл. MiniDumpWriteDump( proc, GetProcessId(proc), file, MiniDumpWithFullMemory, ExceptionInfo ? &info : NULL, NULL, NULL ); CloseHandle(file); return EXCEPTION_CONTINUE_SEARCH; }
Данный модуль создает статический объект coredumpInitializer
, конструктор которого вызывается до функции main()
. Конструктор устанавливает специальный обработчик для системных исключений, в котором в файл и записывается образ памяти текущего процесса при возникновении системных ошибок. Имя файла содержит идентификатор процесса, идентификатор потока и текущее время. Этот файл можно открыть в Visual Studio просто запустив его, либо в самой студии в меню File->Open->Project/Solution
выбрать этот файл, указав его тип как Dump files
. Далее надо запустить программу через отладчик, нажав F5
, и отладчик остановится на месте возникновения ошибки. Естественно, для работы на уровне исходного текста необходимо наличие файла с расширением .PDB, содержащего символьную информацию, в том каталоге, где расположен ваш coredump файл. Файл с раширением .PDB обычно создается при компиляции с включенной отладочной информацией, например, при использования ключа /Zi
.
Надо отметить, что если ошибка в вашей программе произойдет до функции main()
, например, при инициализации каких-то статических объектов, то данный модуль может и не сработать, так как вызов конструктора объекта coredumpInitializer
может быть запланирован уже после проблемного места. Вообще, статические переменные и объекты — это источник многих проблем, в основном из-за неопределенного порядка их инициализации, и их использования стоит избегать по возможности.
Итак, компилируем:
cl /Zi main.cpp coredump.cpp dbghelp.lib
Для компиляции необходима библиотека DbgHelp.lib
. Она обычно входит в состав Windows SDK в составе студии. Также получившийся исполняемый файл main.exe будет использовать динамическую библиотеку DbgHelp.dll
. DbgHelp.dll
входит в состав так называемых redistributable файлов, то есть которые вы имеете право распространять вместе с программой на случай, если у клиента нет этой dll’ки. Эта dll’ка с большой вероятностью находится у вас в каталоге C:\WINDOWS\system32
.
Теперь при запуске программы main.exe
и после закрытия пользователем окна с сообщением об ошибке (см. картинку выше), будет создан .DMP файл с именем, начинающийся со слова coredump, например coredump-4584-3240-20090226022327093.dmp
. Этот файл уже можно открыть в Visual Studio.
Теперь, когда у заказчика программа падает, можно попросить его прислать такой файл для анализа. Единственное, надо хранить .PDB файлы, соответствующие отданным заказчику исполняемым модулям.
Пока я не придумал, как сделать для модуля coredump.cpp
автоматизированный unit-тест. Проблема в том, что тут надо как-то подавить вывод окна с ошибкой (см. картинку выше). Если это сделать, то тест может быть вполне себе автоматизированным.
Более подробная информация вопросу создания образов памяти процесса есть в хорошей статье “Effective minidumps”.
Другие посты по теме:
]]>Menu -> Tools -> Options -> Environment -> Fonts and Colors:
Font -> Fixedsys
TextEditor:
Plain text -> Yellow/Navy
KeyWord -> Lime/(фон оставить по умолчанию)
Конечно, тут есть еще что доработать по мелочам, но лично мне и этого хватает.
А вот для чего я перешел с Professional на Team я расскажу буквально скоро.
]]>Конечно, последней инстанцией будет непосредственно клиент, который непременно сообщит, что программа падает, но всегда хочется минимизировать такие случаи. Хорошо, когда процедура внедрения у заказчика вообще существует. Это позволяет без особых проблем сделать “критическое обновление” уже после релиза. Но в некоторых областях цена допущенных до клиента ошибок крайне высока. Например, для области встраиваемых систем любая минимальная утечка памяти будет фатальной, или, например, для разработчиков игр под игровые консоли будет трудновато “донести” до клиентов критические обновления, необходимость в которых выяснилась сразу после релиза и начала продаж (как это обычно бывает).
Перейдем от слов к делу и рассмотрим конкретный пример работы статического анализатора.
Вот пример “не очень хорошей программы”:
class A { public: A() { char* __p = new char[10]; __p = new char[10]; char* a = (char *)0; *a = 0; char c[10]; c[10] = 0; } ~A() { delete __p; char* a = new char[100]; return; delete[] a; } private: char* __p; }; int main() { A a; return 0; }
Тут без микроскопа видно, что проблем полно:
Утечка памяти в строке 05. Указатель __p
явно имеет неправильное объявление в виде лишнего char*
, которое перекрывает декларацию этого указателя в классе в строке 23. Оператор delete
в строке 15 скорее всего закончится аварийно, так как значение __p
для него будут неопределенно.
Строка 06 присваивает указателю __p
адрес вновь распределенной памяти, тем самым затирая старое значение, присвоенное в строке 05, которое будет потеряно.
Строки 08 и 09 — это обращение по нулевому указателю, приводящее к нарушению защиты памяти.
Строки 12 и 12 — это типичное переполнение буфера (buffer overrun)
Память под указателем в строке 17 никогда не будет освобождена. Это утечка памяти.
Достаточно для начала. Не спорю, пример очень вычурный, но ошибки то весьма типичные, а когда они перемешаны с “правильным” кодом, их обнаружение становится серьезной проблемой.
Теперь возьмем “микроскоп”.
Посмотрим, что сможет сделать для нас Visual Studio. Начиная с версии 2005 у компилятора cl.exe
появился ключ /analyze
, который включает дополнительный анализ и вывод предупреждений о потенциальных проблемах. К сожалению, этот ключ есть только в версии студии Team (в Professional его нет).
Компилируем в Visual Studio 2008 Team:
cl /W3 /O2 /analyze /EHsc bad.cpp
Вот, что дает анализ:
c:\sandbox\analyze\bad.cpp(12) : warning C6201: Index '10' is out of valid index range '0' to '9' for possibly stack allocated buffer 'c'
c:\sandbox\analyze\bad.cpp(5) : warning C6211: Leaking memory '__p' due to an exception. Consider using a local catch block to clean up memory: Lines: 5, 6
c:\sandbox\analyze\bad.cpp(9) : warning C6011: Dereferencing NULL pointer 'a': Lines: 5, 6, 8,9
c:\sandbox\analyze\bad.cpp(12) : warning C6386: Buffer overrun: accessing 'c', the writable size is '10' bytes, but '11' bytes might be written: Lines: 5, 6, 8, 9, 11, 12`
Не так много, как хотелось бы, но хоть что-то. Переполнение буфера в строке 12 обнаружено. Запись по нулевому указателю в строке 09 тоже найдена. Давайте разберемся с сообщением об утечке памяти. Нам сообщается, что возможна утечка, если в строке 06 произойдет исключение (std::bad_alloc
, например), тогда память, распределенная в строке 05 будет потеряна. Это, конечно, проблема, но все-таки суть ошибки передана неверно. Как мне показалось, анализатор в cl.exe
работает последовательно, то есть он следует ходу компиляции, отсюда и “последовательный” характер смысла выведенных предупреждений.
Мы в компании для статического анализа используем Coverity Prevent for C/C++. Есть еще похожий продукт — Klocwork. Эти два продукта делают примерно одну и ту же работу примерно с одинаковым результатом. Мы выбрали первый из-за более подходящей нам ценовой политики и более простого встраивания в систему сборки.
Суть анализа, проводимого данными продуктами, это подобие псевдо-компиляции, когда строится синтаксическое дерево разбора и на основе его проводится анализ всех возможных ветвлений программы. Проходя все ветки, анализатор и проводит свои многочисленные проверки. Прикол в том, что анализ может найти проблему в таком закоулке кода, который может выполняется то раз в год (и раз в год программа падает), и может вы сами никогда не видели, как этот кусок программы работает. Анализ на уровне синтаксиса языка позволяет находить парные проблемы, которые могут быть расположены в разных частях исходного текста (например, поиск несоответствий в конструкторе и деструкторе). Также понимание синтаксиса дает возможность анализировать вложенные вызовы, когда, например, неверный указатель “проявляет” себя только двух или тремя уровнями выше.
Программы-анализаторы типа lint
(или тот же ключ /analyze
), которые просто ищут шаблоны “плохого” кода на уровне лексем, обычно выдаются миллиарды предупреждений, из которых только единицы ценны. При таком подходе разработчику быстро надоедает заниматься выуживанием “жемчужин” из общего потока мусора, и он перестает это делать. Анализаторы же в Coverity и Klocwork выдаются крайне точные сообщения, и процент ложных срабатываний крайне мал (по крайне мере на моем опыте). Также, в каждом из этих продуктов можно самостоятельно настраивать анализатор, фокусируя его на специфичных конкретно для вас потенциальных проблемах, отключая ненужные проверки для уменьшения “шума”.
Идея, лежащая в этих продуктах, это дать не просто нечто, генерирующее тонны текстовых файлов, в которых надо копаться вручную. Тут дается целая среда для автоматизации анализа: групповая работа, система интеграции с контролем версий, позволяющая отдельно проверять каждый внесенный кусок кода и моментально локализовывать время, место и автора “проблем”, система рецензирования когда по исправлению ошибок, общая база данных по ошибкам, которая исключает повторный анализ уже исправленных ошибок, так как положение ошибки характеризуется не просто именем файла и номером строки, на контекстом, и поэтому даже когда ошибка “переехала” в другое место, то он не будет заявлена как новая. Обычно время псевдо-компиляции равно времени вашей обычной сборки, а время самого анализа может занимать в среднем в 3-4 раза дольше. Анализатор прекрасно может использовать многоядерные системы для радикального ускорения процесса. Например, мы с интегрировали статический анализ с системой автоматических “ночных” сборок.
Естественно, никакой анализатор — это не панацея, и все 100% ошибок он не найдет, но изрядную долю выловит, позволив вам потратить освободившееся время на внесение новых ошибок.
Кстати, обе эти конторы всегда организуют бесплатный тест-драйв. Можно попробовать, чего такого интересного сможет найти их анализатор в конкретно вашем коде. Честно могу сказать, это производит впечатление даже на самых заядлых зануд и скептиков среди разработчиков и менеджеров. Когда на ваших глазах открывается такое в коде, что волосы дыбом встают, то к этому невозможно остаться равнодушным. Например, мы сопровождаем большое количество так называемого legacy кода, и тут, конечно, статический анализ проявляет себя во всей красе, хотя и новом, объектно-ориентированном и unit-оттестированном коде тоже бывают ошибки. Это человеческий фактор и от него никуда не деться.
Так вот, анализатор Coverity нашел все проблемы в данной маленькой, но очень плохой программе, включая несоответствие распределения памяти в конструкторе, и ее “неправильном” освобождении в деструкторе. У нас в отделе есть даже специальная копилка, если в твоем коде статический анализатор находит серьезную проблему, типа утечки или какой-нибудь “неприятности” с указателями или памятью, то принято внести в кассу посильную сумму, чтобы ее можно было потратить коллективно при очередном походе в паб. А пабе как-то особенно продуктивно обсуждаются темы типа кто, куда и какую ошибку внес.
Сейчас мы рассмотрели статический анализ кода. Также существует также динамический анализ, когда уже в процессе работы программы специальными средствами производится автоматизированный поиск ошибок. Лично я постоянно использую совершенно волшебный динамический анализатор Valgrind. Valgrind не так удобен, как мне кажется, для полностью автоматизированной проверки и больше подходит, когда надо поймать какой-то конкретный глюк, например, явную утечку памяти, обнаруженную функциональными тестами, но не выявленную статическим анализом.
Отдельной строкой хочу отметить Borland/CodeGear Codeguard, входящий в состав одноименной студии. Данная библиотека может опционально встраиваться борландовым компилятором в код, шпигуя его сотнями проверок на различные утечки, неправильную работу с указателями и прочими неприятностями. Код при этом замедляется в разы и порой делает невозможным отладку вычислительно тяжелых алгоритмов, но вот находимые с помощью Codeguard’а ошибки порой дорогого стоят.
Анализаторы кода (статические или динамические) являются крайне необходимым инструментом. А конкретно, статические, позволяют автоматизировано находится “плохие” места кода, которые проглядели программисты.
]]>T a();
а надо писать:
T a;
так как в первом случае такая запись будет означать декларацию функции a, которая возвращает тип T
, а далеко не декларацию переменной класса T
с вызовом конструктора по умолчанию.
Не спорю, это очевидно для профессионалов. Для новичков же порой подобная “неочевидная” разница вызывает затруднения, поэтому приведу простейший пример, которые расставит все на свои места.
#include <iostream> class T { public: T() { std::cout << "constructor T()"; } }; int main() { std::cout << "T a: "; // Это синтаксис создания экземпляра класса T с вызовом // конструктора по умолчанию. T a; std::cout << std::endl; std::cout << "T b(): "; // А вот это декларация функции "b" без аргументов, // которая возвращает тип T. T b(); std::cout << std::endl; return 0; }
Результат:
T a: constructor T()
T b():
Видно, что для T b();
никакой конструктор не был вызван. Что в целом и ожидалось.
Использование круглых скобок может быть весьма тонким вопросом в С++.
]]>Приведу парочку из загашника. Ни разу не претендую на авторство, так что ценителей авторского права просьба сообщать о возможных нарушениях незамедлительно.
Убирание кнопки Пуск в Windows
Данная программа убирает кнопку Пуск (Start) на 5 секунд, а потом возвращает ее назад. Проверял на Windows 2000 и XP. Если программу прервать в отведенные 5 секунд, кнопку Пуск придется восстанавливать повторным запуском программы
#include <windows.h> int main(void) { // Ищем кнопку. HWND hWnd; hWnd = FindWindow("Shell_TrayWnd", NULL); hWnd = FindWindowEx(hWnd, NULL, "BUTTON", NULL); // Прячем её. ShowWindow(hWnd, SW_HIDE); // Ждём. Sleep(5000); // Показываем обратно. ShowWindow(hWnd, SW_SHOW); return 0; }
Мигание индикаторами на клавиатуре
Данная программа устраивает бегущий огонек по индикаторам NUM LOCK
, CAPS LOCK
и SCROLL LOCK
на более менее обычных клавиатурах.
#include <windows.h> void kbdLight(WORD code) { INPUT input; input.type = INPUT_KEYBOARD; input.ki.wVk = code; input.ki.wScan = 0; input.ki.dwFlags = 0; input.ki.time = 0; input.ki.dwExtraInfo = 0; SendInput(1, &input, sizeof(input)); input.type = INPUT_KEYBOARD; input.ki.wVk = code; input.ki.wScan = 0; input.ki.dwFlags = KEYEVENTF_KEYUP; input.ki.time = 0; input.ki.dwExtraInfo = 0; SendInput(1, &input, sizeof(input)); } int main(void) { while (true) { kbdLight(VK_NUMLOCK); Sleep(100); kbdLight(VK_CAPITAL); Sleep(100); kbdLight(VK_SCROLL); Sleep(100); } return 0; }
Если у вас есть интересные исходнички подобного рода — прикладывайте в комментарии.
Не все же нам в каких-то конструкторах копаться.
]]>new T()
.
Стандарт говорит нам, что если Т
является POD-классом (не объектно-ориентированной сущностью), то объект будет инициализирован значением по умолчанию (обычно, например, для арифметических типов это 0), а если это не POD-класс (явная объектно-ориентированная сущность), то для него вызовется конструктор по умолчанию (либо явный, либо созданный компилятором). Если конструктор по умолчанию задан явно, то будет вызван только он, и вся ответственность за инициализацию ляжет на него. Никой инициализации по умолчанию больше не будет. Если же конструктор по умолчанию не задан явно, и компилятор создал его сам, и в этом случае все члены класса будут проинициализированы неявно: POD-объекты будут проинициализированы нулем, а для не-POD объектов будет проведена инициализация по умолчанию (включая всех его дочерних составляющих — рекурсивный обход всех подобъектов и их инициализация по такому же принципу).
Теперь new T
.
В этом случае для POD-объектов вообще не будет никакой инициализации (что было в памяти на момент распределения, то и будет). Для не POD-объекта просто будет вызван конструктор по умолчанию (либо явный, ли заданный компилятором по умолчанию), и не будет проводиться никакой инициализации POD-составляющих этого объекта.
Для простоты, POD-типами (Plain Old Data) является все наследие языка С в С++. Везде, где есть объектно-ориентированная примесь — это уже не POD-класс. Для не POD-классов нельзя делать никаких предположений о внутренней структуре, расположению в памяти и т.д.
Забавно, структура:
struct A { int b; };
является POD-типом, а вот если добавить в нее, например, слово public
:
struct A { public: int b; };
то по стандарту это не POD-объект, и его нельзя уже трогать на уровне внутреннего представления, например обнулить через memset
. Хотя многие компиляторы разрешают такие “игры” с не POD-объектами и, программа может в принципе работать, но это против стандарта, и, конечно, против переносимости программы.
Итак, описание различий весьма путанное, поэтому лучше рассмотреть пример.
Для чистоты эксперимента я буду использовать так называемое распределение памяти с размещением. То есть я вручную указываю, в каком месте памяти должен будет создаваться объект. Это позволит контролировать “непредсказуемые” значения неинициализированной памяти.
Итак, первый пример:
#include <iostream> #include <cstdlib> class T { public: // Для простоты экспериментируем на однобайтовом типе. unsigned char n; }; int main() { // "Случайная" память для создания объекта. // Берем с запасом, чтобы уж точно вместить объект класса T. char p[10240]; // Заполняем память числом 170 (0xAA) std::memset(p, 170 /* 0xAA */, sizeof(p)); // Создаем объект явно в памяти, заполненной числом 170. T* a = new (p) T; std::cout << "new T: T.n = " << (int)a->n << std::endl; // Заполняем память числом 171 (0xAB) std::memset(p, 171 /* 0xAB */, sizeof(p)); // Создаем объект явно в памяти, заполненной числом 171. T* b = new (p) T(); std::cout << "new T(): T.n = " << (int)b->n << std::endl; return 0; }
Данный пример выведет:
new T: T.n = 170
new T(): T.n = 0
Видно, что для new T
элемент T.n
так остался неинициализированным и равным числу 170
, которые заполнили память заранее. Для new T()
же в свою очередь элемент T.n
стал равны нулю, то есть он был проинициализирован. Все, как сказано в стандарте.
Теперь изменим одну маленькую деталь — добавим в класс Т
явный конструктор:
class T { public: // Явный конструктор. T() {} // Для простоты экспериментируем на однобайтовом типе. unsigned char n; };
Теперь нас ждет сюрприз. Теперь программа будет выводить следующее:
new T: T.n = 170
new T(): T.n = 171
Получается, что даже при new T()
элемент T.n
не был более инициализирован. Почему? А потому, что стандарт гласит: если задан явный конструктор класса, то никакие инициализации по умолчанию для POD-объектов не производятся. Раз программист задал конструктор явно, значит он знает что делает, и вся ответственность за инициализацию теперь на его плечах.
Лично для себя я всегда предпочитаю писать new T()
хотя бы для единообразия вызова конструкторов. Также я всегда явно инициализирую все POD-объекты вручную в конструкторе или в его списке инициализации. Отсутствие предположений о значении POD-типов по умолчанию и инициализация их принудительно позволяет избежать сюрпризов при смене компилятора.
Другие посты по теме:
]]>class A { ... }; class B { public: B(int n); private: A __a; }; B::B(int n) : __a(n) // вызов конструктора А() в списке инициализации. {}
А что произойдет, если в одном из вызовов в списке инициализации произойдет исключение? Например:
class A { public: A(int n) { throw 0; // Конструктор класса А бросает исключение int } }; class B { public: B(int n); private: A __a; }; B::B(int n) : __a(n) // Данный вызов бросает исключение {}
Хотелось бы иметь возможность поймать это исключение и провести “чистку” уже распределенной на тот момент памяти, например:
class P { ... }; class A { public: A(int n) { throw 0; // Конструктор класс А бросает исключение int } }; class B { public: B(); private: P* __p; A __a; }; B::B() : __p(new P), // Память для P распределяется до вызова конструктора класса А __a(0) // Данный вызов бросает исключение {}
На момент, когда конструктор А бросит исключение, мы уже будем иметь распределенную память под указателем __p
и, не обработав исключение, эту память можно потерять.
В С++ есть форма задания try-catch
блока на уровне функции. Используя ее, можно переписать пример так:
#include <iostream> class A { public: A(int n) { throw 0; // Конструктор класс А бросает исключение int } }; class P { public: P() { std::cout << "P(), constructor" << std::endl; } ~P() { std::cout << "~P(), destructor" << std::endl; } }; class B { public: B(); private: P* __p; A __a; }; B::B() try : __p(new P), __a(0) { } catch (int& e) { std::cout << "B(), exception " << e << std::endl; delete __p; }; int main(int argc, char* argv[]) { try { B b; } catch (int& e) { std::cout << "main(), exception " << e << std::endl; } }
Видно (см. тело конструктора B::B()
), что лист инициализации ушел между словом try
и началом try-блока, а тело конструктора теперь внутри try-блока (в данном примере оно пустое), а обработчик исключения находится в catch-блоке после тела конструктора. Данный пример сумеет обработать исключение класса А и освободит память из под указателя __p
. Данный пример выведет следующее:
P(), constructor
B(), exception 0
~P(), destructor
main(), exception 0
Видно, что деструктор класса P
был вызван.
Внимательный читатель заметит, что в функции main()
тоже есть try-блок, а последней строкой программа печатает main(), exception 0
, что значит, что исключение было обработано дважды: в теле try-блока конструктора и затем в функции main()
. Почему?
Правило гласит: исключение, пойманное в обрамляющем функцию виде try-catch
блоке конструктора, будет переброшено еще раз при выходе из конструктора, если конструктор принудительно не сделал это сам, поймав это исключение. Сейчас очень важный момент: если хоть один из членов класса бросил исключение в процессе конструирования, то весь объект принудительно завершает конструирование аварийно с исключением вне зависимости от того, обработано это исключение в конструкторе или нет.
Единственное, что мы тут можем сделать, это “на лету” подправить исключение, брошенное членом класса (например, добавить туда дополнительную информацию). Следующий пример меняет код брошенного классом А
исключения:
#include <iostream> class A { public: A(int n) { throw 0; // Конструктор класс А бросает исключение int } }; class B { public: B(); private: A __a; }; B::B() try : __a(0) { } catch (int& e) { std::cout << "B(), exception " << e << std::endl; e = 1; // Меняем код исключения с 0 на 1. }; int main(int argc, char* argv[]) { try { B b; } catch (int& e) { std::cout << "main(), exception " << e << std::endl; } }
Эта программы выведет следующее:
B(), exception 0
main(), exception 1
Видно, что когда исключение было поймано второй раз, код у него уже не 0 как в оригинальном исключении, а 1.
С конструкторами вроде разобрались. Перейдем к деструкторам.
Деструктор — это тоже функция. К нему тоже применим синтаксис ловли исключения на уровне тела функции, например:
#include <iostream> class B { public: ~B(); }; B::~B() try { throw 2; } catch (int& e) { std::cout << "~B(), exception " << e << std::endl; }
Поведение ловли исключения в деструкторе на уровне функции схоже с конструктором, то есть исключение, пойманное в catch-блоке на уровне функции будет переброшено автоматически снова при завершении деструктора, если он это не сделал сам, обработав исключение. Например:
#include <iostream> class B { public: ~B(); }; B::~B() try { throw 2; } catch (int& e) { std::cout << "~B(), exception " << e << std::endl; } int main(int argc, char* argv[]) { try { B b; } catch (int& e) { std::cout << "main(), B(), exception " << e << std::endl; } }
выведет:
~B(), exception 2 main(), B(), exception 2
то есть исключение, после его обработки в деструкторе было переброшено снова. Конечно, не пойманные исключения в деструкторе являются большим “no-no!” в С++. Принято считать, что не пойманное в деструкторе исключение — это прямой путь к аварийному завершению программы, так как нарушается принцип целостности системы исключений. Если хотите, чтобы ваши программы на С++ работали стабильно, то не допускайте, чтобы исключения “вылетали” из деструктора. Например так:
#include <iostream> class B { public: ~B(); }; B::~B() { try { throw 2; // Бросаем исключение. } catch (int& e) { // И тут же ловим его, не пропуская него “на волю”. std::cout << "~B(), exception " << e << std::endl; } } int main(int argc, char* argv[]) { try { B b; } catch (int& e) { std::cout << "main(), B(), exception " << e << std::endl; } }
Эта программа выведет:
~B(), exception 2
Видно, что исключение не дошло до функции main()
.
С деструкторами тоже вроде разобрались. Теперь перейдем к обычным функциям.
Технику обработки исключений на уровне функции можно применять для любой функции, а не только для конструктора или деструктора, например:
void f() try { throw 1; } catch (int& e) { std::cout << "f(), exception " << e << std::endl; }
Но целесообразность такого синтаксиса сомнительна, так как пойманное исключение не перебрасывается автоматически снова после окончания функции, как это было в случае с конструктором и деструктором. Программа:
#include <iostream> void f() try { throw 1; } catch (int& e) { std::cout << "f(), B(), exception " << e << std::endl; } int main(int argc, char* argv[]) { try { f(); } catch (int& e) { std::cout << "main(), f(), B(0), exception " << e << std::endl; } }
напечатает только:
f(), B(), exception 1
то есть исключение не было передано дальше, поэтому разумнее было бы просто оформить функцию традиционным образом с помощью try-блока, обрамляющего всё тело функции:
void f() { try { throw 1; } catch (int& e) { std::cout << "f(), B(), exception " << e << std::endl; } }
не внося в форматирование текста лишней каши непривычным положением слов try
и catch
.
Лично мне кажется, из всего выше написанного, реально для применения только try-catch
блок на уровне функции для конструктора. Там это действительно актуально, чтобы не допустить объектов, сконструированных только наполовину и убитых в процессе создания исключением от собственного члена (простите за каламбур).
Выводы
Исключения, брошенные при обработке списка инициализации класса можно поймать в теле конструктора через синтаксис try-catch
блока на уровне функции.
Если хоть один элементов класса при конструировании выбросил исключение, то весь класс принудительно завершает собственное конструирование с ошибкой в форме исключения вне зависимости от того, было это исключение поймано в конструкторе или нет.
]]>Часто бывают случаи, когда несколько функций реализуют какую-то единую функциональность, построенную на общем разделяемом ресурсе, защищенном блокировкой. В этом случае каждая функция в начале работы занимает эту блокировку, а на выходе — освобождает ее. Например, методы класса-регистратора системных событий все работают с выходным буфером и используют единую блокировку для синхронизации доступа к нему. Например:
class Logger { public: ... void put(const char* str) { __lock.Lock(); __buffer.push_back(str); __lock.Unlock(); } void flush() { __lock.Lock(); ... __buffer.clear(); __lock.Unlock(); } ... private: Mutex __lock; std::vector<std::string> __buffer; }
В целом, такой подход является не совсем правильным, так как данные методы могут быть весьма сложными, иметь многочисленные условные операторы, могут генерировать исключения. В этом случае программисту необходимо позаботиться о всех возможных вариантах завершения каждой функции и везде вставить оператор освобождения блокировки:
__lock.Unlock();
Если этого не сделать, то неосвобожденная по какой-то редко возникающей причине блокировка может просто “подвесить” всю программу, так как все остальные функции, работающие с этой блокировкой, более никогда не получат управления.
К счастью, в С++ есть механизм, дающий возможность очень просто избежать подобных проблем, вывозом кода освобождения блокировки при любом варианте завершения функции. Механизм называется RAII (Resource Acquisition Is Initialization). В С++ деструкторы созданных в контексте функции объектов обязательно вызываются перед завершением контекста (попросту говоря, когда функция завершается любым способом). Если возникло непойманное в функции исключение, то в процессе раскрутки стека деструкторы созданных локальных объектов тоже будут вызваны. Отсюда и идея: занимать блокировку в конструкторе созданного в функции локального объекта и затем освобождать ее в деструкторе. Использование такого метода позволило бы изменить приведенный пример так:
class Logger { public: ... void put(const char* str) { AutoLock(__lock); __buffer.push_back(str); } void flush() { AutoLock(__lock); ... __buffer.clear(); } ... private: Mutex __lock; std::vector<std::string> __buffer; }
Объект AutoLock
, создаваемый первым в контексте каждой функции, будет занимать блокировку и освобождать ее при закрытии этого контекста.
Идея проста и понятна, а класс, реализующий эту логику еще проще.
Пространство имен ext можно заменить по вкусу на подходящее вам.
Файл autolock.h
:
#ifndef _EXT_AUTOLOCK_H #define _EXT_AUTOLOCK_H #include "mutex.h" namespace ext { class AutoLock { public: // Запираем блокировку в конструторе AutoLock(Mutex& lock) : __lock(lock) { __lock.Lock(); } // Освобождаем блокировку в деструкторе ~AutoLock() { __lock.Unlock(); } private: // Защита от случайного копирования AutoLock(const AutoLock&); void operator=(const AutoLock&); Mutex& __lock; }; } // ext #endif
Данный класс использует реализацию блокировки (мьютекса) Mutex.
Посмотрим, как оно будет в деле (конечно с помощью unit-тестирования).
Традиционно, для компиляции тестов нам нужна Google Test Framework. Как я уже писал, вы можете скачать мою модификацию этой библиотеки, которая сокращена до двух необходимых файлов gtest/gtest.h
и gtest-all.cc
.
Файл autolock_unittest.cpp
:
#include "gtest/gtest.h" #include "autolock.h" #include "mutex.h" #include "thread.h" // Универсальная задержка в миллисекундах для Windows и UNIX #ifdef WIN32 #include <windows.h> #define msleep(x) Sleep(x) #else #include <unistd.h> #define msleep(x) usleep((x)*1000) #endif // Тестовый поток class T: public ext::Thread { public: // Параметры потока: // flag - флаг для сигнализации о выполненном действии // mutex - рабочий объект-блокировка // timeout - время, которое необходимо подождать после // установки флага // val - значение, в которое надо установить флаг T(volatile int& flag, ext::Mutex& mutex, int timeout, int val) : __flag(flag), __mutex(mutex), __timeout(timeout), __val(val) {} // Функция потока: занять автоматическую блокировку, установить // флаг, подождать указанное время, освободить автоматическую // блокировку. virtual void Execute() { ext::AutoLock locker(__mutex); __flag = __val; msleep(__timeout); } private: volatile int& __flag; ext::Mutex& __mutex; int __timeout; int __val; }; // Данный тест выполняет параллельно две функции, которые конкурируют // за одну блокировку. Функция-поток 'a' занимает блокировку, устанавливает // флаг в 1, ждет 100мс и затем освобождает блокировку. Функция-поток 'b' // стартует, когда поток 'a' уже занял блокировку, поэтому после старта // потока 'b' флаг еще некоторое время будет равен 1, пока поток 'a' не // отпустит блокировку, и затем поток 'b' изменит флаг в 0, получив // управление ожидания на блокировке. TEST(AutoLock, ConcurrentCalls) { volatile int flag = 0; ext::Mutex mutex; T a(flag, mutex, 100, 1); T b(flag, mutex, 0, 0); // Запускаем поток 'a'. a.Start(); // Ждем, пока поток 'a' займет блокировку. // Это случится, когда флаг станет равен 1. while (!flag); // Запускаем поток 'b'. b.Start(); // Ждем немного, чтобы убедиться, что поток запустился // и дошел до попытки занять блокировку. msleep(50); // Так как время задержки в потоке 'a' больше 50мс, // то флаг все еще равен 1, так как поток 'a' пока не отпустил // блокировку, не давая потоку 'b' получить управление // и изменить флаг в 0. EXPECT_EQ(1, flag); // Ждем завершения потока 'a' (блокировка должна быть // отпущена при его завершении. a.Join(); // Ждем завершения потока 'b', который к своему завершению // должен обнулить флаг. b.Join(); EXPECT_EQ(0, flag); }
Для компиляции нам также понадобятся файлы mutex.h
(класс Mutex), thread.cpp
и thread.h
(класс Thread).
Файл для запуска тестов runner.cpp
:
#include "gtest/gtest.h" int main(int argc, char* argv[]) { testing::InitGoogleTest(&argc, argv); return RUN_ALL_TESTS(); }
Компилируем:
Visual Studio:
cl /EHsc /I. /Feautolock_unittest_vs2008.exe /DWIN32 runner.cpp autolock_unittest.cpp thread.cpp gtest\gtest-all.cc
Cygwin:
cl /EHsc /I. /Feautolock_unittest_vs2008.exe /DWIN32 runner.cpp autolock_unittest.cpp thread.cpp gtest\gtest-all.cc
Запускаем:
[==========] Running 1 test from 1 test case.
[----------] Global test environment set-up.
[----------] 1 test from AutoLock
[ RUN ] AutoLock.ConcurrentCalls
[ OK ] AutoLock.ConcurrentCalls
[----------] Global test environment tear-down
[==========] 1 test from 1 test case ran.
[ PASSED ] 1 test.
Работает, что приятно. Тест работает как ожидалось.
Не забудьте включить файл autolock_unittest.cpp
в тестовый набор вашего проекта. Не тратьте время на вылавливание неожиданных глюков тогда, когда вы уже порядком подзабыли, как тут все работает. Пусть ловлей глюков занимается тест, автоматизировано.
Другие посты по теме:
]]>QNX/UNIX. Анатомия параллелизма
Хоть я в многопотоковом программировании далеко не новичок, я прочитал эту книгу от корки до корки, дважды. С удовольствием цеплялся на каждый абзац, перечитывал, обдумывал примеры, чтобы ничего не пропустить, компилировал их, крутил туда-сюда, сравнивал результаты с книгой, пробовал на разных системах и т.д.
Несмотря на присутствие слова QNX в названии книги, она является прекрасным руководством по программированию потоков в UNIX в целом. Начиная прямо от печки, то есть от определения потока и его порой неверного отделения от термина “процесс”, далее, следуя по базовым способам синхронизации потоков (блокировки, семафоры, условные переменные), детально разжеваны все тонкости поточного программирования с точки зрения идей и концепций, и в конкретном применении их в потоках POSIX (pthreads), включая нестандартные расширения различных версий.
Отдельной строкой укажу на отличную главу про взаимодействие механизма сигналов UNIX’а и, собственно, потоков. Это весьма сложная тема, но тут она понятно расписана. Даны рекомендации “как надо” и “как не надо”.
Полно показательных примеров на С++. Причем примеры не просто призваны показать типа “о! работает в потоке!”. Примеры демонстрируют особенности, проблемы потоков, позволяют оценить ресурсоемкость различных приемов. Везде авторы дают рекомендации типа как ускорить данный кусок кода, как его упростить, как сделать его надежнее, как его тестировать.
Даются сравнительные оценки работы потоков в различных UNIX’ах, например, QNX против Linux. Все плюсы и минусы обстоятельно, без эмоций разобраны. В конце книги рассматриваются некоторые чисто QNX’овые возможности: пулы потоков, и как QNX избавляет программиста от огромного количества головной боли при их использовании, методика программирования сервисов на основе сообщений (для QNX это вообще родная тема благодаря микроядру).
Авторы совершенно точно смогли сконцентрироваться именно на теме многопоточности, не тратя место в книге (всего ~280 страниц) на смежные вопросы, предоставив для “открытых” вопросов отличную библиографию.
Есть книги, которые, как говориться, прочитал, передай другому, а некоторые уже не хочется никому отдавать. Это одна из таких немногих книг.
]]>Есть замечательная программа Rapid Environment Editor (RapidEE). Она позволяет очень удобно редактировать переменные окружения Windows в виде двух панелей: слева системные переменные, справа пользователькие. Переменная PATH автоматически представляется в виде списка путей. И даже то, что всю картину переменных видно как на ладони, и то, что программа понимает вставку из буфера обмена — все это меркнет перед гениальной функцией подсветки “мертвых” путей в переменной PATH. “Мертвым” путь может быть в основном по двум причинам: либо путь остался от программы, которая давно снесена, либо путь просто задан неверно, а вы битый час пытаетесь понять, почему что-то там не запускается. RapidEE моментально решает подобные проблемы.
Я не сторонник графического интерфейса, и чего греха таить, люблю командную строку. В свое время я написал небольшой скрипт, который в Windows распечатывает пути из переменной PATH по отдельности:
Файл splitpath.cmd
:
@echo off set line=%path% setlocal :parse_line for /F "delims=; tokens=1,*" %%a in ("%line%") do ( echo %%a set line=%%b ) if "%line%" NEQ "" goto parse_line endlocal
Примерный результат его работы выглядит так:
C:\WINDOWS\system32
C:\WINDOWS
C:\WINDOWS\System32\Wbem
c:\Python25
C:\Program Files\CodeGear\RAD Studio\5.0\bin
C:\Program Files\Java\jdk1.6.0_04
C:\Program Files\Java\jdk1.6.0_04\jre\bin
C:\Program Files\PC Connectivity Solution
c:\oracle\9.2.0.1\bin
C:\Program Files\Oracle\jre\1.3.1\bin
C:\Program Files\Oracle\jre\1.1.8\bin
C:\WINDOWS\Microsoft.NET\Framework\v1.1.4322
C:\PROGRA~1\Borland\CBUILD~1\Bin
Но после перехода на RapidEE нужда в скрипте отпала совсем.
Вывод: RapidEE очень удобная программа для редактирования переменных окружения в Windows. Программа бесплатная. Может работать в portable режиме и не требовать установки.
Скриншот:
]]>minmax.cpp
):
#include <algorithm> int main() { int a = std::min(10, 20); return 0; }
Все тривиально и отлично компилируется и в Visual Studio, и в CodeGear/Borland Studio, и Cygwin. Но допустим потребовались какие-то функции из Windows API, и вы подключили файл windows.h
:
Теперь компиляция в Visual Studio (я проверял в 2005 и 2008) будет падать со следующей ошибкой:
minmax.cpp
minmax.cpp(4) : error C2589: '(' : illegal token on right side of '::'
minmax.cpp(4) : error C2059: syntax error : '::'
Постановка #include <windows.h>
до #include <algorithm>
проблемы не решает.
Очевидно, проблема в том, что кто-то переопределил значение слова min. Запустим препроцессор и проверим догадку:
cl /P minmax.cpp
И что мы видим? А видим мы следующее (фрагмент файла minmap.i
):
#line 7 "minmax.cpp" int main() { int a = std::(((10) < (20)) ? (10) : (20)); return 0; }
Естественно, это каша с точки зрения синтаксиса, и компилятор ругается совершенно законно.
Покопавшись в заголовочных файлах Windows SDK, в файле WinDef.h
, который косвенно подключается через windows.h
, я нашел корень зла:
#ifndef NOMINMAX #ifndef max #define max(a,b) (((a) > (b)) ? (a) : (b)) #endif #ifndef min #define min(a,b) (((a) < (b)) ? (a) : (b)) #endif #endif /* NOMINMAX */
Вот теперь ясно, что делать — надо определить макрос NOMINMAX, тем самым заблокировать определение min
и max
:
#define NOMINMAX #include <algorithm> #include <windows.h> int main() { int a = std::min(10, 20); return 0; }
Забавно, что в Cygwin и CodeGear/Borland исходный пример компилируется без проблем. В борландовой версии windows.h
я нашел вот такой фрагмент:
#if defined(__BORLANDC__) ... # if defined(__cplusplus) # define NOMINMAX /* for WINDEF.H */ ... # endif ... #endif /* __BORLANDC__ */
Эдак они заранее оградились от проблемы, принудительно запретив проблемные макросы.
Вывод: Порой промежуточные результаты работы препроцессора являются крайне полезной информацией.
На всякий случай напомню, как его запускать для перечисленных мной компиляторов:
Visual Studio:
cl.exe /P имя_исходника.cpp
Borland/CodeGear Studio:
cpp32.exe имя_исходника.cpp
Cygwin:
cpp.exe имя_исходника.cpp
Прочие флаги командной строки должны повторять флаги при обычной компиляции. Для препроцессора важны определения макросов (обычно это флаги -D
и -U
) и пути для поиска включаемых файлов (обычно это флаг -I
).
Другие посты по теме:
]]>std::cout << std::hex << std::uppercase << std::setfill('0') << std::setw(2) << 0xAA;
Причем std::setw()
надо повторять для каждого нового выводимого элемента. Я свел все это в один итератор, чтобы можно было просто написать (указав итератору ширину выводимого поля):
std::cout << ext::Hex(2) << 0xAA;
Итак, класс Hex
(название пространства имен можно подкрутить по вкусу), файл hex.h
:
#ifndef _EXT_HEX_H #define _EXT_HEX_H #include <iostream> #include <iomanip> namespace ext { class Hex { public: Hex(int width) : __width(width) {} friend std::ostream& operator<< (std::ostream& os, const Hex& hex); private: int __width; }; inline std::ostream& operator<< (std::ostream& os, const Hex& hex) { std::hex(os); std::uppercase(os); os.width(hex.__width); os.fill('0'); return os; } } // ext #endif // _EXT_HEX_H
Теперь можно писать так:
std::cout << ext::Hex(0) << 0x0a << std::endl; std::cout << ext::Hex(1) << 0x0a << std::endl; std::cout << ext::Hex(1) << 0xaa << std::endl; std::cout << ext::Hex(2) << 0xaa << std::endl; std::cout << ext::Hex(4) << 0xaa << std::endl; std::cout << ext::Hex(8) << 0x0a << std::endl; std::cout << ext::Hex(16) << 0x0a << std::endl; std::cout << ext::Hex(32) << 0x0a << std::endl;
И результатом будет:
A
A
AA
AA
00AA
0000000A
000000000000000A
0000000000000000000000000000000A
На всякий случай, unit-тест. Чтобы не было сюрпризов при обновлении компилятора, STLport или чего-то еще. Тест всегда проверит, работает ли класс так, как вы от него ждете. Вы можете возразить — ну класс-то выеденного яйца не стоит, а тут для него тесты… Соглашусь. А еще я соглашусь, что сотни раз самые казалось бы ненужные на первый взгляд тесты для “очевидных” классов помогали обнаружить глюки на новой версии системных библиотек, новой версии компилятора, использовании “более мощных” параметров оптимизации и т.д. Время на написание тестов всегда окупается сполна, всегда.
Традиционно, для компиляции тестов нам нужна Google Test Framework. Как я уже писал, вы можете скачать мою модификацию этой библиотеки, которая сокращена до двух необходимых файлов gtest/gtest.h
и gtest-all.cc
.
Файл hex_unittest.cpp
:
#include "gtest/gtest.h" #include "hex.h" #include <sstream> void testHex(int n, int w, const std::string& etalon) { std::stringstream fmt; fmt << ext::Hex(w) << n; EXPECT_EQ(etalon, fmt.str()); } TEST(HexManip, Generic) { testHex(0x0A, 0, "A"); testHex(0x0A, 1, "A"); testHex(0xAA, 1, "AA"); testHex(0xAA, 2, "AA"); testHex(0xAA, 4, "00AA"); testHex(0xAA, 8, "000000AA"); testHex(0xAA, 16, "00000000000000AA"); testHex(0xAA, 32, "000000000000000000000000000000AA"); } Ну и головная программа: #include "gtest/gtest.h" int main(int argc, char* argv[]) { testing::InitGoogleTest(&argc, argv); return RUN_ALL_TESTS(); }
Компилируем.
Visual Studio:
cl /EHsc /I. /Fehex_unittest_vs2008.exe runner.cpp hex_unittest.cpp gtest\gtest-all.cc
Cygwin:
g++ -I. -o hex_unittest_cygwin.exe runner.cpp hex_unittest.cpp gtest/gtest-all.cc
Запускаем:
[==========] Running 1 test from 1 test case.
[----------] Global test environment set-up.
[----------] 1 test from HexManip
[ RUN ] HexManip.Generic
[ OK ] HexManip.Generic
[----------] Global test environment tear-down
[==========] 1 test from 1 test case ran.
[ PASSED ] 1 test.
Работает как положено.
При использовании Hex
у себя в проекте не забудьте включить файл hex_unittest.cpp
в ваш набор unit-тестов. Оберегите себя от ненужной траты времени в будущем.
Под занавес пара слов о производительности. Очевидно, что если вы выводите в поток десятки тысяч шестнадцатеричных чисел подряд, то разумнее будет использовать стандартные итераторы — настроить поток с помощью std::hex
, std::uppercase
и std::setfill
, а потом вызывать только std::setw
для каждого нового элемента. Но если вы печатаете разнородные данные, что часто требуется при отладке, то тогда итератор Hex
будет в самый раз.
Другие посты по теме:
]]>Интерфейс USB. Практика использования и программирования
Очень целостная книга про USB от электрических основ и применяемых микросхем до написания драйверов под Windows. Я прочитал книгу на одном дыхании, и не пожалел ни капли о потраченном времени. Правда, если быть честным, то последние главы про написание драйверов под Windows я уже просматривал по диагонали, ехидно хихикая про себя на тему “как же можно было усложнить написание драйверов под винды…” и почему в libusb так просто и понятно даже с нуля, а в Windows DDK проще использовать всякие конструкторы драйверов для радикального сокращения времени “начального вхождения” в тему. Но это мои личные тараканы.
Прочитав книгу вы как минимум точно будете знать почему конкретно нельзя два компьютера просто взять и соединить обычным USB кабелем. Я, например, со своим программистским сознанием недоумевал раньше, мол почему если принтер можно подсоединить к компьютеру по USB, то почему же нельзя вместо принтера поставить другой компьютер, написав для него программу по аналогии с принтерной прошивкой, и организовать тем самым мини сеть? Это же просто вопрос драйверов (я так думал)… А тут меня заставляют покупать какой-то хитрый кабель с логикой внутри…
В общем, за себя могу сказать — я на капельку поумнел, что приятно.
А если серьезно, то прочитав эту книгу, можно спокойно самостоятельно “набросать” USB-устройство и написать для него драйвера по Windows.
Жалко, что в книге рассмотрено написание USB драйверов только под Windows. Было бы интересно написать один и то же драйвер под Windows и Linux, например, и оценить трудозатраты.
]]>Бывают случаи, когда параметр шаблона сам является шаблонным классом и для его инстанцирования тоже нужно указать параметр. Например, универсальная шаблонная функция для печати стандартного контейнера любого типа в поток:
template< typename C, typename E > void print(const C<E>& v) { copy(v.begin(), v.end(), ostream_iterator<E>(cout, " ")); cout << endl; }
И все бы ничего, но с только зрения синтаксиса С++ это неверно. Нельзя просто написать C<E>
, если E
сам является не определенным типом, а параметром шаблона. Правильный способ использования параметра шаблона, который в свою очередь зависит от другого параметра, должен выглядеть так:
template< template<typename> class C, typename E > void print(const C<E>& v) { copy(v.begin(), v.end(), ostream_iterator<E>(cout, " ")); cout << endl; }
Теперь полный пример (template_parameter.cpp
):
#include <iostream> #include <iomanip> #include <algorithm> #include <iterator> #include <string> #include <vector> #include <list> #include <deque> // Я обычно не использую пространства имен "по умолчанию", но тут // это сделано для компактности примера. using namespace std; // Вся изюминка тут: template<typename> или template<class>. // Без этого параметр шаблона "С" нельзя будет параметризировать. // в конструкции C<E>&. template< template<typename> class C, typename E > // Тут происходит параметризация параметра "С" параметром "E". // Без этого класс "С" не может быть использован, так как "E" // является не просто типом, а тоже параметром шаблона. void print(const C<E>& v) { // Так как класс элемента контейнера "Е" нам тут нужен как отдельный // тип, то для этого и затеяна вся тема с параметризированными // параметрами шаблона. copy(v.begin(), v.end(), ostream_iterator<E>(cout, " ")); cout << endl; } // Тестовая программа демонстрирует, как одна функция print() // может использоваться для печати любого контейнера // (если, конечно, он удовлетворяет требованиям алгоритма // copy() по наличию должных итераторов), содержащего элементы // любого типа. int main(int argc, char* argv[]) { // Массив целых. int i[5] = { 1, 2, 3, 4, 5 }; // Создаем вектор, состоящий из целых, и печатаем его. print< vector, int >( vector<int>(i, i + 5) ); // Массив вещественных. float f[5] = { .1, .2, .3, .4, .5 }; // Создаем вектор, состоящий из вещественных, и печатаем его. print< vector, float >( vector<float>(f, f + 5) ); // Массив символов. char c[5] = { 'a', 'b', 'c', 'd', 'e' }; // Создаем деку, состоящую их символов, и печатаем ее. print< deque, char >( deque<char>(c, c + 5) ); // Массив строк в стиле С. char* s[5] = { "a1", "b2", "c3", "d4", "e5" }; // Создаем список, состоящий из строк, и печатаем его. print< list, string >( list<string>(s, s + 5) ); return 0; }
Компилируем.
Cygwin:
g++ -o template_parameter_cygwin.exe template_parameter.cpp
или в Borland/Codegear Studio 2007:
bcc32 /etemplate_parameter_cg2007.exe template_parameter.cpp
И запускаем скомпилированный файл:
1 2 3 4 5
0.1 0.2 0.3 0.4 0.5
a b c d e
a1 b2 c3 d4 e5
Отчетливо видно, что на первой строке распечатаны целые, на второй вещественные, на третьей символы, и на четвертой строки.
Вы спросите, где компиляция в Visual Studio? А вот с ней вышел облом. Я пробовал скомпилировать этот пример в Visual Studio 2005 и 2008, и в обоих случаях я получал ошибки типа:
template_as_parameter.cpp(38) : error C3208: 'print' : template parameter list for class template 'std::vector' does not match template parameter list for template template parameter 'C'
Из чего я сделал вывод, что микрософтовский компилятор не поддерживает подобный синтаксис.
Я был очень расстроен подобным фактом, так как в целом очень положительно отношусь к cl.exe
. А тут выходит, что даже борландовый компилятор это понимает, а cl.exe
нет. Если кто знает, может есть ключик какой секретный для включения поддержки “хитрых и редких” возможностей С++ в компиляторе микрософта — научите, пожалуйста. Буду очень признателен.
Предвосхищу вопросы типа “зачем так сложно, да еще и плохо переносимо” — все верно. Лично я бы отнес все выше описанное к “темным углам” С++, но уж больно интересно по ним полазать.
Обновление
Комментарий Александра прояснил ситуацию с проблемой при компиляции в Visual Studio. Окончательный вариант кода, чтобы работало в cl.exe
, таков:
template< template<typename, typename> class C, typename E > void print(const C<E, allocator<E> >& v) { copy(v.begin(), v.end(), ostream_iterator<E>(cout, " ")); cout << endl; }
У шаблонов стандартных контейнеров есть второй параметр, так называемый allocator
. Этот параметр часто используется со значением по умолчанию, поэтому редко приходится вспоминать о нем. И как уточнил Александр, моя проблема была в том, что cl.exe
требует явного указания наличия этого параметра при параметризации параметра C
.
Исправленный код компилируется во всех опробованных компиляторах, теперь включая и cl.exe
.
Другие посты по теме:
]]>Как я уже писал, я работаю одновременно на совершенно разных платформах — от виндов до встраеваемого QNX’а. Поэтому мне всегда хочется иметь максимально простые и переносимые исходники (и желательно с минимумом нестандартных зависимостей), чтобы модули можно было просто кидать из проекта в проект, с платформы на платформу, не допиливая каждый раз что-то напильником.
Итак, привожу ниже класс, который я использую для получения информации об ошибке, произошедшей в операционной системе. Можно узнать код ошибки и его текстовое объяснение, если оно предоставляется. Это не бог весть какой сложный и оригинальный класс, но у меня он работает без каких-либо “допиливаний” на Windows 32- и 64-бит, Linux 2.6 32- 64-бит SPARC и Intel, Sun OS 5.10 SPARC и Intel, AIX, HP-UX и HP-UX IA64. К тому же, этот класс безопасен для мультипотокового использования (что лично для меня, например, очень важно).
Итак, класс SystemMessage
. Все члены статические, так что можно работать с ними без создания экземпляра класса.
Пространство имен, как обычно, ext, так что измените, если необходимо.
Файл systemmessage.h
:
#ifndef _EXT_SYSTEM_MESSAGE_H #define _EXT_SYSTEM_MESSAGE_H #include <string> namespace ext { class SystemMessage { public: // Эта функция возращает код ошибки. static int code(); // Эта функция по коду ошибки возвращает ее текстовое описание, если // таковое предоставляется операционной системой. Если нет, то // возвращается строка "?". static std::string message(int code); }; } // namespace ext #endif // _EXT_SYSTEM_MESSAGE_H
Файл systemmessage.cpp
:
#include "SystemMessage.h" #ifdef WIN32 #include <windows.h> #else #include <string.h> #include <unistd.h> #include <errno.h> #endif namespace ext { int SystemMessage::code() { #ifdef WIN32 return GetLastError(); #else return errno; #endif } // Если система по какой-то причине не имеет функции strerror_r, // то придется лазить напрямую в таблицу сообщений об ошибках. // Для этого надо при компиляции определить макрос LIBC_NO_STRERROR_R. // Пока я видел такое только на HP-UX IA64 v2. #ifndef WIN32 #ifndef LIBC_NO_STRERROR_R extern "C" int sys_nerr; extern "C" char* sys_errlist[]; #endif #endif std::string SystemMessage::message(int code) { char msg[1024]; #ifdef WIN32 // Версия для Windows FormatMessage( FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, NULL, code, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), msg, sizeof(msg) - 1, NULL ); char* p = msg + strlen(msg); // Обрезаем c конца '\r', '\n' и '.' for(p = msg + strlen(msg) - 1; p >= msg && (*p == '\n' || *p == '\r' || *p == '.'); --p) *p = 0; #elif LIBC_NO_STRERROR_R // Если UNIX-платформа не имеет функции strerror_r, то делаем ее // работу вручную. Пока я встретил такое только на HP-UX IA64 v2. if (code < 0 || code >= sys_nerr) return "?"; strncpy(msg, sys_errlist[code], sizeof(msg) - 1); // Если сообщение об ошибке длинее чем sizeof(msg)-1, то '\0' // не будет скопирован, поэтому добавляем его вручну. msg[sizeof(msg) - 1] = 0; #else // Для нормальной UNIX-системы просто вызываем strerror_r. if (strerror_r(code, msg, sizeof(msg) - 1) < 0) return "?"; #endif // Поможем компилятору по возможности оптимизировать // возвращаемое значение как rvalue. return std::string(msg); } } // namespace ext
Теперь посмотрим это в работе.
Я как-то пока не придумал, как универсально написать unit-тест для этого класса, так как предсказуемые результаты будут все равно различны для каждой платформы. А писать тесты под все платформы как-то топорно. Хочется гармонии, а тут пока ее нет. Если кто имеет идею, как универсально тестировать этот класс на всех платформах — поделитесь, пожалуйста.
Тестовая программа systemmessage_test.cpp
:
#include <iostream> #include <fstream> #include "systemmessage.h" int main(int argc, char* argv[]) { // Пытаемся открыть заведомо несуществующий файл. std::ifstream is("__non_existing_file__"); // Печатаем ошибку. int error = ext::SystemMessage::code(); std::cout << error << ", " << ext::SystemMessage::message(error) << std::endl; return 0; }
Компилируем в Visual Studio:
cl /EHsc /Fesystemmessage_test_vs2008.exe /DWIN32 systemmessage_test.cpp systemmessage.cpp
Запускаем systemmessage_test_vs2008.exe
:
2, The system cannot find the file specified
Получили примерно ожидаемое виндовое сообщение об ошибке.
Теперь компилируем в Cygwin:
g++ -o systemmessage_test_cygwin.exe systemmessage_test.cpp systemmessage.cpp
Запускаем systemmessage_test_cygwin.exe
:
2, No such file or directory
Получили сообщение об ошибке в стиле UNIX.
Повторюсь, в данном классе нет ничего удивительного и сложного. Просто это весьма универсальный и переносимый исходник.
И небольшая ремарка. В мире UNIX существует два диалекта функции strerror_r
: XSI-версия (когда определен макрос _XOPEN_SOURCE
, и он равен 600) и GNU-версия (доступная в libc, начиная с версии 2.0). Разница в том, что первая (XSI-версия) просто кладет сообщение об ошибке в предоставленный буфер и также возвращает код успешности или неуспешности своей работы в виде int
‘а. Нормальный UNIX-подход. Вторая версия (GNU) возвращает не int
, а, собственно, указатель на строку с ошибкой, причем указываеть он может как на предоставленный функции буфер, так и куда-то еще, например, на какой-то внутренний буфер. Данный класс рассчитан на работу с XSI-версией функции strerror_r
. Поэтому, если вдруг при компиляции этого класс на UNIX-системах вы получите сообщение об ошибке в использовании этой функции, то определите макрос _XOPEN_SOURCE
в значение 600 (-D_XOPEN_SOURCE=600
для компилятора), тем самым будет принудительно использоваться XSI-версия этой болезной функции.
Интересно, что настоящим автором Нортон Коммандера является далеко не сам Питер Нортон, а John Socha. Именно он создал изначальную версию, которая называлась VDOS (Virtual DOS), еще будучи студентом. Следуя именно идее “виртуального ДОСа”, первая версия коммандера имела файловые панели только на половину экрана — вторая нижняя часть была “виртуальным окном” в ДОС. Сейчас это окно обычно сокращают до одной командной строки. Затем Джон Сόха присоединился к Peter Norton Computing, и программа начала свое триумфальное шествие по планете под именем Norton Commander. Последней версией коммандера, вышедшей из под рук Джона, была версия 3.0. Именно она является “классической” и именно её знало большинство российских пользователей IBM PC. Но эта версия была лебединой песней коммандера. Питер Нортон продал компанию Symantec’у, а Джон Сόха уволился, прекратив работу над коммандером. После версии 3.0 в Symantec выпустили еще пару версий, но время уходило. Оригинальный интерфейс версии 3.0 был “улучшен”, программа заметно потолстела и замедлилась, а на пятки уже наступали Дос Навигатор и Волков Коммандер, а в новом 32-битном мире — первые версии FAR и Total (Windows) Commander.
Вообще, история создания эпохального файлового менеджера весьма интересна.
На мой взгляд, именно представление файловой структуры в виде двух панелей с возможностью адресовать файлы с одной панели на другую, дополненная мгновенным редактором и просмотрщиком, является наиболее удобной для программистской работы, когда необходимо прыгать между десятком файлов одновременно, чего-то временно скопировать/переименовать/удалить, тут что-то быстренько отредактировать, в другом месте поискать и т.д. С трудом могу представить все эти действия через однопанельный файловый а-ля explorer, где только для копирования файла надо сначала мышкой на него указать, взять “на копирование”, открыть второй explorer с местом назначения и сделать туда “Paste”. Например, замечаю на собой постоянно, если мне надо подправить какие-либо настройки проекта в Visual Studio (например, пути), то мне гораздо быстрее переключиться в FAR, там через F4 открыть файл проекта, найти нужное место и исправить прямого в тексте конфигурации, чем ползать через окошки и менюшки самой IDE. Чего уж говорить об удобстве встроенного редактора, особенно если установлена подсветка синтаксиса. Я много раз пытался заставить себя по F4 вызывать внешний редактор, типа Notepad++, но все равно скатывался до встроенного, ибо он вызывается мгновенно. Notepad++, конечно, отличная программа, но в ней я только делаю “сложную” поиск-замену, когда нужны регулярные выражения.
Мир UNIX’а тоже не обделен хорошими двухпанельными файловыми менеджерами, реально ускоряющими процесс, когда надо сотни раз повторять cd/ls/cp/mv/cd/ls/cp/mv…, разруливая какой-нибудь завал на файловой системе. Midnight Commander знают все. Радостно, что за последние несколько месяцев снова началась активная работа над проектом, знамя которого лежало без движения уже несколько лет. Но есть еще один заслуживающий внимания проект. Неутомимый Сергей Вакуленко создал и развивает Bash Commander. Сейчас это патч к официальной версии Bash, но Сергей бьется за включения его в общее дерево. Этот патч ненавязчиво добавляет в Bash то, что там так не хватает: при нажатии на Ctrl-O (^O) появляются заветные две панели. То есть пока идет неспешная программерская работа в стиле make/vi, то все как обычно в командной строке шелла, но как только надо муторно повозиться с множеством файлов, то нажимаем Ctrl-O, и далее привычные панели с клавишам F1-F10. Интерфейс, конечно, крайне минималистический и не имеет всех наворотов MC, но есть один плюс - крайне простая сборка. Везде, где собирается Bash, там можно собрать и Bash Commander, чего нельзя сказать MC с длинным списков зависимостей, который просто собирается разве что под линуксом, а вот на динозаврах типа AIX’а или HP-UX’а становится грустно. Функциональность Bash Commander’а легко расширяется при помощи самих же скриптов на Bash. Надеюсь, что работу Сергея таки включат в официальную версию Bash.
Мне кажется, что наш постсоветский IBM-PC’шный мир привил некоторым из нас не самую плохую привычку к двухпанельным текстовым файловым менеджерам, не так ли?
]]>К сожалению для Москвы этот день будет не столь особым, так как там это произойдет уже 14-го в 02:31:30.
Это, наверное, как наблюдать затмение, в некотором цифровом виде.
Для отслеживания обратного отсчета сего события есть забавная страничка с говорящим адресом: http://coolepochcountdown.com/
Всегда любил игры в числа.
]]>std::ifstream is("testfile.txt"); std::string val( (std::istream_iterator<char>(is)), std::istream_iterator<char>() );
Скотт Мейерс в книге “Эффективное использование STL. Библиотека программиста” в Совете 6 “Остерегайтесь странностей лексического разбора C++” (стр. 42, изд. “Питер” 2002) дает исчерпывающее объяснение этого “феномена”. Ответ крайне меня опечалил, так как вскрыл некоторую нелогичность и корявость в целом стройного и красивого языка С++ в данном вопросе. Очевидно, что причины этого в сохранения в С++ обратной совместимости с С, но от этого не легче.
Итак, давайте разберемся по порядку (чтобы меня не обвинили в плагиате, сразу скажу, что я буду следовать примерному тексту Мейерса, так как он дал великолепное объяснение с примерами, и изобретать велосипед в данном случае было бы неразумно). Как мы предполагали, код std::istream_iterator<char>(is)
создает экземпляр потокового итератора, привязанного к потоку is
. И все бы ничего, если такая конструкция используется как самостоятельное объявление. Вся проблема в именно в использовании такого выражения в контексте вызова функции (в данном случае, конструктора), то есть в качестве параметра. Мейерс приводит следующий пример:
int f(double d);
Это команда объявления функции f
, которая получает double
и возвращает int
.
Тоже самое происходит и в следующей строке. Круглые скобки вокруг имени параметра d
не нужны, поэтому компилятор их игнорирует:
int f(double (d)); // То же; круглые скобки вокруг d игнорируются
Теперь третий вариант объявления той же функции. В нем имя параметра просто не указано:
int f(double); // То же; имя параметра не указано
Три такие формы объявления знакомы всем, хотя про возможность заключения параметра в скобки знают не все (может просто потому, что это очевидно лишние по логике вещей скобки).
Теперь можно рассмотреть еще три объявления функции. В первом объявляется функция g
с параметром — указателем на функцию, которая вызывается без параметров и возвращает double
:
int g(double (*pf)()); // Функции g передается указатель на функцию
То же самое можно сформулировать иначе. Единственное различие заключается в том, что pf
объявляется в синтаксисе без указателей (допустимом как в С, так и в С++):
int g(double pf()); // То же; pf неявно интерпретируется как указатель
Как обычно, имена параметров могут опускаться, поэтому возможен и третий вариант объявления g
без указания имени pf
:
int g(double()); // То же; имя параметра не указано
Обратите внимание на различия между круглыми скобками вокруг имени параметра (например, параметра d
во втором объявлении f
) и стоящими отдельно (как в этом примере). Круглые скобки, в которые заключено имя параметра, игнорируются, а стоящие отдельно, обозначают присутствие списка параметров; они сообщают о присутствии параметра, который является указателем на функцию.
Теперь вернемся к оригинальному примеру:
std::ifstream is("testfile.txt"); std::string val( std::istream_iterator<char>(is), std::istream_iterator<char>() );
Сейчас я намеренно убрал таинственные “лишние” скобки вокруг первого параметра.
Что же перед нами тут? Совершенно не то, о чем мы думали изначально. Перед нами объявление функции val
, возвращающей тип std::string
. Функция получает два параметра:
is
, относится к типу istream_iterator<char>
. Лишние круглые скобки вокруг is
игнорируются.istream_iterator<char>
.А мы то тут ожидали увидеть описание вызова конструктора, которому передаются два потоковых итератора. Такая интерпретация написанного диктуется одним из основных правил C++: все, что может интерпретироваться как указатель функцию, должно интерпретироваться именно так. Так гласит стандарт:
В грамматике имеется неоднозначность, когда инструкция может быть выражением, так и объявлением. Если выражение с явным преобразованием типов в стиле вызова функции (expr.type.conv) является крайним слева, то оно может быть неотличимо от объявления, в котором первый оператор объявления начинается с открытой круглой скобки “(”. В этом случае инструкция рассматривается как объявление. — [C++03] п.6.8.
Так что же делают эти магические скобки вокруг первого параметра конструктора?
std::ifstream is("testfile.txt"); std::string val( (std::istream_iterator<char>(is)), std::istream_iterator<char>() );
А вот что — объявления формальных параметров не могут заключаться в круглые скобки, я вот заключить в круглые скобки аргумент при вызове функции можно. Вот эти круглые скобки и помогают компилятору решить неоднозначность в нужную нам сторону (а не как положено по стандарту по умолчанию) и точно указать, что перед нами именно использование параметра функции при ее вызове, а не при объявлении.
Соглашусь, от этого может слегка заболеть голова, причем совершенно без причины.
Как написал Герб Саттер в книге “Новые сложные задачи на С++” (он тоже посвятил этому вопросу целую главу, “Задача 23. Инициализация ли это?”, стр. 192, изд. “Вильямс”), что такие моменты синтаксиса С++ являются его “темными углами”, и их стоит избегать. Рассмотренный пример можно упростить, объявив итератор отдельно, а не прямо в тексте вызова конструктора, тем самым не заходить в “темный угол”. Не так элегантно, зато просто и понятно:
std::ifstream is("testfile.txt"); std::istream_iterator<char> begin(is); std::istream_iterator<char> end; std::string val(begin, end);
Читал я недавно, как Линус Торвальдс полоскал С++ за неоправданную языковую сложность. “C++ is a horrible language!”, — сказал Линус. Может он и прав.
Мыши плакали, кололись, но продолжали грызть С++.
Другие посты по теме:
]]>php
это делается так:
$lines = file_get_contents("textfile.txt");
или так, медленно и неэффективно, по-старинке:
$lines = join("", file("textfile.txt"));
Задумался я, как бы это так элегантно одним оператором сделать в С++. Естественно, хочется, чтобы это работало приемлемо быстро для больших файлов (например, от одного мегабайта и больше).
Первое, что сходу приходит в голову (универсальная кондовая классика):
std::ifstream is("testfile.txt"); std::string v; char buf[N]; while (is) { is.read(buf, sizeof(buf)); v.append(buf, is.gcount()); }
С размером буфера N можно еще проиграться, выбрав оптимальный.
Первое улучшение, приходящее в голову — это распределить заранее внутренний буфер для строки, минимизировав количество перераспределений буфера в процессе чтения. Для этого мы, естественно, должны заранее знать размер читаемого файла. Пусть это будет 1 мегабайт (1024*1024).
std::ifstream is("testfile.txt"); std::string v; // Резервируем внутренний буфер std::string на указанный // размер в один мегабайт. v.reserve(1024*1024); char buf[N]; while (is) { is.read(buf, sizeof(buf)); v.append(buf, is.gcount()); }
Далее на сцену выходят STL-алгоритмы и потоковые итераторы. Берем типовой пример использования алгоритма std::copy
, который есть практически в любой книге по С++:
std::ifstream is("testfile.txt"); std::string v; // Сброс данного флага необходим для отключения пропуска пробельных // символов при форматном вводе через поток. is.unsetf(std::ios::skipws); std::copy( std::istream_iterator<char>(is), std::istream_iterator<char>(), std::back_inserter(v) );
Сразу скажу, это крайне медленный метод. Первым, приходящим в голову улучшением, как всегда, является предварительное распределение буфера приемной строки:
std::ifstream is("testfile.txt"); std::string v; // Резервируем внутренний буфер std::string на указанный // размер в один мегабайт. v.reserve(1024*1024); is.unsetf(std::ios::skipws); std::copy( std::istream_iterator<char>(is), std::istream_iterator<char>(), std::back_inserter(v) );
В рассмотренном методе видно, что сначала данные изымаются из потока потоковым итератором istream_iterator
, а потом кладутся через итератор строки back_inserter
в саму строку. Двойная работа. Есть метод лучше — класть данные из потока напрямую в строку, используя один из специальных конструкторов класса std::string
:
std::ifstream is("testfile.txt"); is.unsetf(std::ios::skipws); // Самое интересное происходит тут: создается переменная "v" // через конструктор, работающий напрямую с итераторами потока, // и данные напрямую поступают во внутренний буфер строки. std::string v( (std::istream_iterator<char>(is)), std::istream_iterator<char>() );
Опытный читатель заметит казалось бы ненужные обрамляющие скобки вокруг первого параметра. Сразу скажу — без них работать не будет, а будет ошибка компиляции. Тут мы касаемся одного из “темных углов” С++. Это не самый очевидный вопрос, поэтому я посвятил ему отдельную статью.
Уже лучше, но двигаемся дальше. В потоках ввода есть специальные итераторы istreambuf_iterator
, которые работают напрямую с внутренними буферами потока в обход всех высокоуровневых функций форматирования и выходного преобразования. Именно по этому для них вызов функции unsetf
будет уже не нужен:
std::ifstream is("testfile.txt"); std::string v; // Опциональное резервирование буфера приемной строки. v.reserve(1024*1024); std::copy( std::istreambuf_iterator<char>(is), std::istreambuf_iterator<char>(), std::back_inserter(v) );
И теперь вариант через конструктор класса std::string:
std::ifstream is("testfile.txt"); std::string v( (std::istreambuf_iterator<char>(is)), std::istreambuf_iterator<char>() );
Мы уже близки к идеалу. Теперь встроим создание объекта std::ifstream
прямо в код создания строки:
std::string v( (std::istreambuf_iterator<char>( std::ifstream("testfile.txt") )), std::istreambuf_iterator<char>() );
Мы уже в миллиметре от идеала, но в приведенном примере есть одно большое “но”. Вызов std::ifstream("testfile.txt")
прямо в вызове конструктора создает временный объект, который по стандарту языка всегда является константой, а первый параметр конструктора ожидает принять не константный параметр, поэтому “строгий” компилятор типа gcc скорее всего выдаст ошибку компиляции, а менее “строгий”, например cl.exe
от Майкрософта на такой вызов не ругается. Но мы не можем принять такое не универсальное решение, поэтому изменим код, чтобы параметр создавался динамически в куче, а для автоматического его удаления будет использоваться std::auto_ptr
:
std::string v( (std::istreambuf_iterator<char>( *(std::auto_ptr<std::ifstream>( new std::ifstream("testfile.txt") )).get() )), std::istreambuf_iterator<char>() );
Этот код должен работать в любом стандартном компиляторе С++.
Оглядимся назад. У нас столько вариантов — какой выбрать? Для начала, скорость. Надо понять, какой вариант работает банально быстрее. Для этого я собрал все эти варианты в тестовую программу (конечно, с использованием Google Test Framework).
Как я уже писал, вы можете скачать мою модификацию этой библиотеки, которая сокращена до двух необходимых файлов gtest/gtest.h
и gtest-all.cc
.
filereader_unittest.cpp
:
#include <gtest/gtest.h> #include <iostream> #include <streambuf> #include <istream> #include <fstream> #include <ios> #include <iomanip> #include <string> #include <vector> #include <memory> #include <cstdlib> // Управляющий класс для нашей среды тестирования. class Env: public testing::Environment { public: // Размер тестового файла: 1 мегабайт. static int testfile_sz() { return 1024 * 1024; } // Имя тестового файла. static const char* testfile() { return "testfile"; } protected: // Эта функция вызывается один раз в начале тестирования. // Она создает тестовый файл. void SetUp() { std::string dummy(testfile_sz(), 'x'); std::ofstream os(testfile()); os.write(dummy.c_str(), dummy.length()); } // Эта функция вызывается один раз после всех тестов. // Она удаляет тестовый файл. void TearDown() { std::remove(testfile()); } }; // Функция, реализующая классический метод чтения файла кусками // заданной длины N. При необходимости производится предварительное // распределение буфера приемной строки. void rawRead(int N, bool reserve) { std::ifstream is(Env::testfile()); std::string v; if (reserve) v.reserve(Env::testfile_sz()); char* buf = new char[N]; while (is) { is.read(buf, sizeof(buf)); v.append(buf, is.gcount()); } delete[] buf; // На всякий случай проверяем размер считанного файла. EXPECT_EQ(Env::testfile_sz(), v.length()); } // Классическое чтение с буфером в 100 байт. TEST(ReaderTest, raw_100) { rawRead(100, false); } // Классическое чтение с буфером в 1 килобайт. TEST(ReaderTest, raw_1024) { rawRead(1024, false); } // Классическое чтение с буфером в 10 килобайт. TEST(ReaderTest, raw_10240) { rawRead(10240, false); } // Классическое чтение с буфером в 10 килобайт с предварительным // распределением буфера приемной строки. TEST(ReaderTest, raw_reserve_10240) { rawRead(10240, true); } // Функция, реализующая чтение через итератор istream_iterator. // При необходимости производится предварительное распределение // буфера приемной строки. void check_istream_iterator(bool reserve) { std::ifstream is(Env::testfile()); std::string v; if (reserve) v.reserve(Env::testfile_sz()); // Принудительное игнорирование пропуска пробельных символов. // С этим флагом двоичные данные будут читаться неверно. is.unsetf(std::ios::skipws); std::copy( std::istream_iterator<char>(is), std::istream_iterator<char>(), std::back_inserter(v) ); // На всякий случай проверяем размер считанного файла. EXPECT_EQ(Env::testfile_sz(), v.length()); } // Тестируем работу через istream_iterator. TEST(ReaderTest, istream_iterator) { check_istream_iterator(false); } // Тестируем работу через istream_iterator с предварительным // распределением буфера приемной строки. TEST(ReaderTest, istream_iterator_reserve) { check_istream_iterator(true); } // Тестируем работу через istream_iterator при прямом // вызове конструктора строки, который берет данные напрямую // из итераторов. TEST(ReaderTest, istream_iterator_tostring) { std::ifstream is(Env::testfile()); is.unsetf(std::ios::skipws); std::string v( (std::istream_iterator<char>(is)), std::istream_iterator<char>() ); // На всякий случай проверяем размер считанного файла. EXPECT_EQ(Env::testfile_sz(), v.length()); } // Функция, реализующая чтение через итератор istreambuf_iterator. // При необходимости производится предварительное распределение // буфера приемной строки. Для данного метода сброс флага // std::ios::skipws не нужен, так как этот итератор работает // на более низком уровне. void check_istreambuf_iterator(bool reserve) { std::ifstream is(Env::testfile()); std::string v; if (reserve) v.reserve(Env::testfile_sz()); std::copy( std::istreambuf_iterator<char>(is), std::istreambuf_iterator<char>(), std::back_inserter(v) ); // На всякий случай проверяем размер считанного файла. EXPECT_EQ(Env::testfile_sz(), v.length()); } // Тестируем работу через istreambuf_iterator. TEST(ReaderTest, istreambuf_iterator) { check_istreambuf_iterator(false); } // Тестируем работу через istreambuf_iterator с предварительным // распределением буфера приемной строки. TEST(ReaderTest, istreambuf_iterator_reserve) { check_istreambuf_iterator(true); } // Тестируем работу через istreambuf_iterator при прямом // вызове конструктора строки, который берет данные напрямую // из итераторов. TEST(ReaderTest, istreambuf_iterator_tostring) { std::ifstream is(Env::testfile()); std::string v( (std::istreambuf_iterator<char>(is)), std::istreambuf_iterator<char>() ); // На всякий случай проверяем размер считанного файла. EXPECT_EQ(Env::testfile_sz(), v.length()); } #ifdef WIN32 // Этот тест аналогичен тесту istreambuf_iterator_tostring // за исключение создания объекта потока прямо в вызове // конструктора строки. Работает только в cl.exe, так как // "стандартный" компилятор запрещает передавать временные // объекты по неконстантной ссылке, а cl.exe почему-то это // разрешает. TEST(ReaderTest, istreambuf_iterator_tostring_short) { std::string v( (std::istreambuf_iterator<char>( std::ifstream(Env::testfile()) )), std::istreambuf_iterator<char>() ); // На всякий случай проверяем размер считанного файла. EXPECT_EQ(Env::testfile_sz(), v.length()); } #endif // Финальный метод. Конструктор строки берет данные напрямую // из итератора istreambuf_iterator. Объект потока создается // динамически прямо в коде вызова конструктора строки через // std::auto_ptr. TEST(ReaderTest, istreambuf_iterator_tostring_short_auto_ptr) { std::string v( (std::istreambuf_iterator<char>( *(std::auto_ptr<std::ifstream>( new std::ifstream(Env::testfile()) )).get() )), std::istreambuf_iterator<char>() ); // На всякий случай проверяем размер считанного файла. EXPECT_EQ(Env::testfile_sz(), v.length()); } // Запуск тестов. int main(int argc, char **argv) { // Инициализация нашей тестовой среды. testing::AddGlobalTestEnvironment(new Env); testing::InitGoogleTest(&argc, argv); return RUN_ALL_TESTS(); }
Для компиляции вам необходимы файлы gtest/gtest.h
и gtest-all.cc
(см. выше).
Для начала скомпилируем в Visual Studio 2008:
cl /Fefilereader_vs2008.exe /DWIN32 /O2 /arch:SSE2 /I. /EHsc filereader_unittest.cpp gtest-all.cc
Запускаем:
filereader_vs2008.exe --gtest_print_time
Опция --gtest_print_time
указывает Google Test выводить время работы каждого теста.
Результат:
[==========] Running 12 tests from 1 test case.
[----------] Global test environment set-up.
[----------] 12 tests from ReaderTest
[ RUN ] ReaderTest.raw_100
[ OK ] ReaderTest.raw_100 (141 ms)
[ RUN ] ReaderTest.raw_1024
[ OK ] ReaderTest.raw_1024 (94 ms)
[ RUN ] ReaderTest.raw_10240
[ OK ] ReaderTest.raw_10240 (109 ms)
[ RUN ] ReaderTest.raw_reserve_10240
[ OK ] ReaderTest.raw_reserve_10240 (94 ms)
[ RUN ] ReaderTest.istream_iterator
[ OK ] ReaderTest.istream_iterator (359 ms)
[ RUN ] ReaderTest.istream_iterator_reserve
[ OK ] ReaderTest.istream_iterator_reserve (344 ms)
[ RUN ] ReaderTest.istream_iterator_tostring
[ OK ] ReaderTest.istream_iterator_tostring (281 ms)
[ RUN ] ReaderTest.istreambuf_iterator
[ OK ] ReaderTest.istreambuf_iterator (141 ms)
[ RUN ] ReaderTest.istreambuf_iterator_reserve
[ OK ] ReaderTest.istreambuf_iterator_reserve (125 ms)
[ RUN ] ReaderTest.istreambuf_iterator_tostring
[ OK ] ReaderTest.istreambuf_iterator_tostring (78 ms)
[ RUN ] ReaderTest.istreambuf_iterator_tostring_short
[ OK ] ReaderTest.istreambuf_iterator_tostring_short (67 ms)
[ RUN ] ReaderTest.istreambuf_iterator_tostring_short_auto_ptr
[ OK ] ReaderTest.istreambuf_iterator_tostring_short_auto_ptr (78 ms)
[----------] 12 tests from ReaderTest (1891 ms total)
[----------] Global test environment tear-down
[==========] 12 tests from 1 test case ran. (1906 ms total)
[ PASSED ] 12 tests.
Давайте проанализируем результаты.
Мы не будем сравнивать абсолютные времена компиляторов друг против друга. Сейчас не об этом.
Вывод первый. Предварительное резервирование буфера приемной строки (метод reserve()
) не дает никакого эффекта в нашем случае. Может это из-за того, что стратегия расширения буфера при простом линейном добавлении данных итак весьма эффективна в классе std::string
.
Вывод второй. Размер буфера чтения в методе чтения файла явными кусками установленного размера не дал четкой картины. Не очевидно, какой размер буфера может быть потенциально оптимальным. Тут может и дисковый кэш повлиял, может внутреннее буферизирование в классе std::ifstream
, может что-то еще.
Вывод третий. Работа через итератор istream_iterator
является крайне медленной. Возможно это связано с накладными расходами на форматные преобразования, производимые данным классом и совершенно ненужные в нашей задаче. Для реального использования данный метод практически непригоден.
Вывод четвертый. Использование конструктора класса std::string
, работающего напрямую с итераторами потока, заметно быстрее, чем использование алгоритма std::copy
(ReaderTest.istream_iterator
заметно медленнее ReaderTest.istream_iterator_tostring
и ReaderTestiterator
заметно медленнее ReaderTest.istreambuf_iterator_tostring
). И понятно почему — данные напрямую поступают в буфер строки без ненужного промежуточного копирования.
Вывод пятый (основной). Метод чтения через итератор istreambuf_iterator
с использованием конструктора строки, работающего напрямую с итераторами потока (тест ReaderTest.istreambuf_iterator_tostring
для “нестрогого” компилятора и тест ReaderTest.istreambuf_iterator_tostring_auto_ptr
для компилятора, следующего стандартам), является весьма эффективным и может конкурировать с ручным блочным чтением. Конечно, текст данного метода весьма непрост и может запутан для понимания на первый взгляд, особенно для начинающих, а блочное чтения прозрачно и ясно, но при почти равной эффективности этих методов нет причин отказываться от работы через итераторы, так как данный метод весь фактически предоставляется библиотекой STL, а значит быть может оптимизирован независимо, без затрагивания кода уже использующей его программы.
Другие посты по теме:
]]>Например, в программе есть некоторая строковая переменная, хранящая описание текущего состояния. Это состояние может, например, выводиться в нижней полоске рабочего окна. Теперь представим, что в программе есть два параллельно работающих потока. Первый занимается получением данных из сети, а второй — обработкой базы данных. Допустим, настал некоторый момент времени, когда первый поток принял данные из сети и хочет об этом отрапортовать в строке состояния, записав туда “Принято 16384 байт”. Приблизительно в этот же момент второй поток завершил периодическую проверку базы данных и также желает об этом сообщить пользователю строкой “База данных проверена”. Операция копирования строки не является атомарной, то есть нет никакой гарантии, что во время ее выполнения процессор не переключится на какой-то другой поток, и операция копирования не будет прервана посреди работы. Итак, когда поток номер 1 успел записать в строку состояния слово “Принято”, может так случиться, что процессор активирует поток номер 2, который также начнет запись своей строки и добавит к уже записанному “Принято” строку “База данных про”, но будет прерван первым потоком и т.д. В итоге в переменная может содержать кашу типа “ПрияноБаза данных 1про6в3ерена84 байт”. Вывод такой — результат полностью непредсказуем.
Для решения подобного вроде проблем в мире параллельного программирования существует такое понятие, как блокировка. Суть ее в том, что когда один процесс захватывает блокировку, то все остальные процессы, пытающиеся ее захватить после, будут блокированы до тех пор, пока первый процесс ее не отпустит. Это похоже на дверь в комнату: представим, что наша переменная globalStatus
находится в комнате с одной дверью и ключом внутри. Если дверь открыта (блокировка свободна), то в комнате никого нет (никто не работает с переменной). Когда процесс заходит в комнату, он запирает дверь изнутри (захватывает блокировку). После этого процесс может спокойно работать с переменной как угодно долго, так как гарантированно никто другой не войдет в комнату, так как она заперта изнутри, и не будет мешать ему работать с переменной.
Это была идея простейшей блокировки, которую часто называют мьютекс (mutex). Сейчас мы рассмотрим реализацию такой блокировки на С++, которая будет работать в Windows и UNIX. Как я писал в статье про параллельные потоки, в мире UNIX стандартом де-факто является библиотека pthread (POSIX Threads). Имеено ее мы и будем использовать для UNIX-версии. Для Windows будет отдельная реализация.
Класс Mutex получился весьма простой, в виде единственного файла mutex.h
. Пространство имен (namespace) называется ext для простоты. Переименуйте его, если это требуется для вашего проекта.
mutex.h
:
#ifndef _EXT_MUTEX_H #define _EXT_MUTEX_H #ifdef WIN32 #define WIN32_LEAN_AND_MEAN #define NOGDI #include <windows.h> #else #include <stdlib.h> #include <pthread.h> #endif namespace ext { #ifdef WIN32 typedef CRITICAL_SECTION MutexType; #else typedef pthread_mutex_t MutexType; #endif // Интерфейс класса Mutex. // Класс задумывался как маленький и быстрый, поэтому все // определено прямо в заголовочном файле, и все функции // объявлены принудительно inline. Это должно уберечь // от ошибок и предупреждений о двойных символах при // включении mutex.h в несколько модулей. class Mutex { public: inline Mutex(); // Деструктор объявлен как не виртуальный из-за тех же // соображений эффективности. Если вы планируете // наследоваться от этого класса, то лучше сделать // деструктор виртуальным, так как наследование от класса // с не виртуальным деструктором потенциально опасно // с точки зрения утечек памяти и является одним из // больших "no-no" в С++. inline ~Mutex(); // Функция захвата блокировки (вход в комнату и запирание // двери ключом изнутри). inline void Lock(); // Функция освобождения блокировки (отпирание двери и // выход из комнаты) inline void Unlock(); private: MutexType __mutex; // Защита от случайного копирования объекта данного класса. // Экземпляр этого класса с трудом может быть нормально // скопирован, так как он жестко привязан к системному // ресурсу. Mutex(const Mutex&); void operator=(const Mutex&); }; #ifdef WIN32 // Реализация через Windows API Mutex::Mutex() { InitializeCriticalSection(&__mutex); } Mutex::~Mutex() { DeleteCriticalSection(&__mutex); } void Mutex::Lock() { EnterCriticalSection(&__mutex); } void Mutex::Unlock() { LeaveCriticalSection(&__mutex); } #else // WIN32 // UNIX версия через pthread Mutex::Mutex() { pthread_mutex_init(&__mutex, NULL); } Mutex::~Mutex() { pthread_mutex_destroy(&__mutex); } void Mutex::Lock() { pthread_mutex_lock(&__mutex); } void Mutex::Unlock() { pthread_mutex_unlock(&__mutex); } #endif // WIN32 } // ext #endif
Касаемо техники “защиты” объекта в С++ от случайного копирования я уже писал ранее.
Я не стал проверять коды возвратов данных функций для упрощения класса. Могу сказать, что если хоть одна из них завершиться с ошибкой, то это значит, что-то конкретно не так в вашей системе, и приложение по любому не будет работать нормально еще по миллиарду причин.
Пощупаем класс в работе. И конечно, используя unit-тестирование.
Традиционно, для компиляции тестов нам нужна Google Test Framework. Как я уже писал, вы можете скачать мою модификацию этой библиотеки, которая сокращена до двух необходимых файлов gtest/gtest.h
и gtest-all.cc
.
Для теста нам также потребуются файлы thread.cpp и thread.h из статьи про параллельные потоки в С++.
mutex_unittest.cpp
:
#include "gtest/gtest.h" #include "mutex.h" #include "thread.h" // Макрос для осуществления задержки в миллисекундах #ifdef WIN32 #include <windows.h> #define msleep(x) Sleep(x) #else #include <unistd.h> #define msleep(x) usleep((x)*1000) #endif // Определим параллельный поток, который будет // "конкурировать" с основным потоком за блокировку. // Данный поток будет пытаться захватить блокировку, // изменить значение флага и освободить затем // блокировку. class A: public ext::Thread { public: // Передаем в конструкторе ссылку на флаг и // ссылку на блокировку. A(volatile int& flag, ext::Mutex& mutex) : __flag(flag), __mutex(mutex) {} virtual void Execute() { // Захват блокировки (1) __mutex.Lock(); // Изменяет флаг на 1 __flag = 1; // Освобождаем блокировку __mutex.Unlock(); } private: volatile int& __flag; ext::Mutex& __mutex; }; TEST(MutexTest, Generic) { // Начальное значение флага - 0. volatile int flag = 0; // Создаем объект-блокировку ext::Mutex mutex; // Захватываем блокировку. mutex.Lock(); // Создаем параллельный поток выполнения. A a(flag, mutex); // Запускаем его. a.Start(); // Ждем для проформы десятую секунды, чтобы дать // время параллельному потоку создаться и успеть // дойти до строки (1), то есть до захвата блокировки. msleep(100); // Значение флага должно быть все еще 0, так как // параллельный поток должен быть блокирован на // строке (1), так как мы захватили блокировку еще // до его создания. EXPECT_EQ(0, flag); // Освобождаем блокировку, тем самым давая // параллельному потоку выполняться дальше и // изменить значение флага на 1. mutex.Unlock(); // Ждем завершения параллельного потока. a.Join(); // Так как параллельный поток завершился, то // флаг теперь точно должен быть равен 1. EXPECT_EQ(1, flag); }
Для запуска тестов нам нужен стандартный файл запуска runner.cpp
:
#include "gtest/gtest.h" int main(int argc, char* argv[]) { testing::InitGoogleTest(&argc, argv); return RUN_ALL_TESTS(); }
Компилируем все вместе.
В Visual Studio:
cl /EHsc /I. /Femutex_unittest_vs2008.exe /DWIN32 runner.cpp mutex_unittest.cpp thread.cpp gtest\gtest-all.cc
Или если вы используете gcc
:
g++ -I. -o mutex_unittest_cygwin.exe runner.cpp mutex_unittest.cpp thread.cpp gtest/gtest-all.cc
Запускаем mutex_unittest_vs2008.exe или mutex_unittest_cygwin.exe:
[==========] Running 1 test from 1 test case.
[----------] Global test environment set-up.
[----------] 1 test from MutexText
[ RUN ] MutexText.Generic
[ OK ] MutexText.Generic
[----------] Global test environment tear-down
[==========] 1 test from 1 test case ran.
[ PASSED ] 1 test.
Вроде работает как надо.
Теперь внесем в исходный текст класса “случайную” ошибку, заменив строку:
void Mutex::Lock() { EnterCriticalSection(&__mutex); }
на
void Mutex::Lock() { /* EnterCriticalSection(&__mutex); */ }
Этой “ошибкой” мы просто отключили создание блокировки. Перекомпилируем все заново и запустим:
[==========] Running 1 test from 1 test case.
[----------] Global test environment set-up.
[----------] 1 test from MutexText
[ RUN ] MutexText.Generic
mutex_unittest.cpp:41: Failure
Value of: flag
Actual: 1
Expected: 0
[ FAILED ] MutexText.Generic
[----------] Global test environment tear-down
[==========] 1 test from 1 test case ran.
[ PASSED ] 0 tests.
[ FAILED ] 1 test, listed below:
[ FAILED ] MutexText.Generic
1 FAILED TEST
Видно, что флаг был изменен в параллельном потоке вне зависимости от блокировки (и понятно почему, мы ж ее “сломали”).
Итак, можно вернуть исправленную строку в исходное состояние. Класс работает, и тесты на это подтвердили.
При использовании класса Mutex у себя в проекте не забудьте включить файл mutex_unittest.cpp
в ваш набор unit-тестов.
В завершении могу сказать, что данный класс успешно работает и проверен мной лично на Windows (32- и 64-бит), Linux 2.6 (32- и 64-бит Intel и SPARC), AIX 5.3 и 6, SunOS 5.2 64-bit SPARC, HP-UX и HP-UX IA64.
Другие посты по теме:
]]>static void sleepMs(int ms);
Эта функция реализует задержку в указанное число миллисекунд. Функция является статической, то есть ей можно пользоваться без создания экземпляра касса:
ext::PreciseTimer::sleepMs(100);
Необходимо учитывать, что в UNIX системах данная функция может быть прервана пришедшим системным сигналом, например, сигналом о полученных новых данных в буфер сокета. В этом случае задержка может быть меньне, чем ожидается.
Обновленный исходный текст класса и тестов находится по старому адресу.
]]>Дармаван Салихан
BIOS. Дизассемблирование, модификация, программирование (BIOS Disassembly Ninjutsu Uncovered)
С изматывающими подробностями разжевываются по крупицам вопросы, касаемые биоса. Начиная от организации памяти в писюках 90-х и биосов тех лет вплоть современных матерей со всем последними шинами, протоколами, системам загрузки, инициализации железа, загрузки микрокода в процессор и периферию, ориентации на модное направление встраиваемых систем и т.д. Изучается вопрос написания своих биосов с нуля (как на ассемблере, так и на си) и модификации существующих. Подробно разобран пример, как всем известный вирус CIH стирал биосы на некоторых матерях своего времени.
Изложение материала сопровождается изобилием примеров программ и результатов дизассемблирования под винды и линукс.
Единственный вопрос, который, как мне показалось, остался за бортом — это альтернативы классическому подходу в простроении биосов на основе ядра линукса, а именно системы Coreboot, ранее известной как LinuxBIOS.
Резюме: крайне занимательное чтиво для занимающихся встраиваемыми системами, и вообще находящихся на стыке железа и софта.
И еще. Нельзя забывать о том, что книги надо не только иметь, но и читать.
]]>В процессе внесения мноего патча в Google Test Framework какой-то китаец просто извел меня своими придирками не только в фундаментальному качеству представленных мной unit-тестов, доказывающих правильность моих изменений, но и вплоть до знаков пунктуации в комментариях. Лишь раза с десятого, он сказал “поехали!”, и махнул рукой, как говорится. Патч то был размером строк в 50-60. По первости я просто бесился, что какой-то хрен указывает мне на мнимые “недостатки” моего “гениального кода”. Но когда в результате я оглянулся на то, что было изначально и на результат — я простил ему все.
В программировании, как в любой другой науке, чем дальше ты растешь и развиваешься, чем очевиднее становится факт наличия людей, гораздо более крутых в некоторых вопросах чем ты. И, как мне кажется, очень важно прислушиваться к советам, а особенно, претензиям, исходящих от них.
]]>std::vector
для хранения указателей на размещенные в куче объекты.
class Book { public: Book(int index); ... }; ... std::vector<Book *> books; for (int i = 0; i < 10; ++i) books.push_back(new Book(i));
Естественно, после использования память надо освободить. Обычно стандартный прием для этого таков:
for (std::vector<Book *>::iterator i = books.begin(); i != books.end(); ++i) delete *i;
В целом, с таким подходом все в порядке, разве что слегка веет от него излишней алгоритмической загруженностью. Он вынужден, навязан особенностями языка C++. Индексная переменная i
здесь абсолютно неважна для цикла, она является чисто служебной. Все это, конечно, не так страшно, как использование оператора goto или статических переменных, но все равно хочется гармонии. И способ есть. Данный код можно переписать так:
#include <algorithm> class deleter { public: template <typename T> void operator()(const T* p) const { delete p; } }; ... std::for_each(a.begin(), a.end(), deleter());
Данный код определяет класс-функтор, у которого перегруженный оператор operator()
является шаблонным. Затем стандартный алгоритм std::for_each()
вызывает этот оператор для каждого члена вектора.
Конечно, вы можете сказать, что мол битва за идею принуждает нас таскать за собой класс deleter
, но тут аргумент простой — данный подход ближе к декларативному подходу в программировании, нежели к прямому алгоритмическому. В декларативном подходе вы стараетесь как можно больше логики перенести из ее явного программирования базовыми конструкциями типа условий, циклов и т.д. к ее выражению через определения (декларации) сущностей и их взаимосвязей. Декларативные конструкции проще дробить на независимые куски, а значит проще тестировать. Например, вы можете протестировать алгоритм std::for_each
в изоляции, тем самым гарантируя его корректную работу сразу во всей программе, а вот протестировать явный цикл в изоляции вряд ли получится, так как цикл “жестко вплетен” в прочую логику программы. Максимум удастся проверить данный конкретный цикл как-то вручную, и если их программе много, проверять придется каждый из них.
Соглашусь, однако, что конкретно этот пример весьма тривиален и является в большинстве делом вкуса, нежели вопросом реального выигрыша простоте и тестируемости кода. Но сам прием весьма показателен в плане замены простейших алгоритмов высокоуровневыми сущностями. И еще, в защиту такого приему могу сказать, что например, вы можете переопределить алгоритм std::for_each
на свой, который сможет на конкретно вашей платформе выполняться гораздо быстрее, или, например, ловить исключения работы с кучей и журналировать проблемы освобождения памяти. В случае же прямого использования цикла for
вам придется переписать сам цикл. Хорошо, когда такое место одно в программе, а если их тысячи?
putenv()
. Она устанавливает переменную окружения для текущего процесса. В основном функция полезна, когда ваш процесс вызывает из под себя другой процесс и передает ему какие-либо параметры через переменные окружения.
Прототип функции такой:
int putenv(char* string);
Казалось бы, все предельно просто и понятно — вызываешь функцию, она делает свое дело, если надо проверятся код ошибки, и все пучком.
Например:
putenv("TESTVAR=testvalue");
Или так:
... char var[1024]; ... strcpy(var, "TESTVAR="); strcat(var, value); putenv(var); ...
Вроде похожие примеры, хотя между ними есть огромное “но”. Первый пример будет работать нормально, а вот второй пример может приводить к неверному значению установленной переменной или даже к падению программы с нарушением защиты памяти на некоторых платформах. Почему?
Отгадка кроется в том, что на некоторых операционных системах и версиях системной библиотеки libc значение параметра будет использоваться напрямую даже после возврата из функции. Другими словами, функция putenv()
не копирует переданный ей параметр куда-либо. Если вы передадите в эту функцию буфер, размещенный в стеке (автоматическая переменная), то это прямой путь к серьезным ошибкам памяти, так в момент реального использования установленной переменной автоматический буфер может уже не существовать (функция, его создавшая, уже завершила работу и подчистила за собой стек). Отсюда вывод — указатель, передаваемый в функцию putenv()
, всегда должен указывать на статические данные. Первый пример работает правильно, так как в С и C++ строковые константы размещены в статическом сегменте и постоянны во время всей жизни программы.
Ситуация усугубляется еще и тем, что разные версии системной библиотеки ведут себя по разному. Нет четкого правила: копировать значение буфера или нет. Я лично наступил на эти грабли, когда начал писать программу под виндами на Visual Studio, все работало. А вот при запуске на юниксах или под виндами, но использовании компилятора gcc из Cygwin, все падало. Конечно, чтение документации все сразу прояснило, но вот осадочек остался. Просто использование буфера напрямую, без создания внутренней копии, как-то противоречит здравому смыслу, но… как сделало, так сделано.
Я набросал мини-тесты на всю эту тему.
Традиционно, для компиляции тестов нам нужна Google Test Framework. Как я уже писал, вы можете скачать мою модификацию этой библиотеки, которая сокращена до двух необходимых файлов gtest/gtest.h
и gtest-all.cc
.
Файл тестов (putenv_unittest.cpp
):
#include <gtest/gtest.h> #include <cstdlib> // Главная проверочная функция. Вызывает putenv() с указанным буфером // (автоматическим или статическим), если надо перезатирает буфер, чтобы // проверить тот ли это буфер, и после сверяет установленное значение // с ожидаемым эталоном. void checkPutEnv(char* buf, bool rewrite) { // Эталон const char* etalon = "TESTVAR=testvalue"; // Копируем эталон в буфер std::strcpy(buf, etalon); // Устанавливаем переменную putenv(buf); // Если того требуют условия теста, то перезатираем буфер // фальшивым значением. if (rewrite) std::strcpy(buf, "TESTVAR=novalue"); // Копируем значение системной переменной. std::string actual = getenv("TESTVAR"); // Сверяем результаты EXPECT_EQ(std::string("testvalue"), actual); } // Тест с использованием статического буфера без перезатирания. // Это должно работать стопудово на все платформах, так как // используется статический буфер, и мы его не перезатираем. TEST(PutEnv, UsingStaticVariable) { static char buf[1024]; checkPutEnv(buf, false); } // Тест с использованием статического буфера с перезатиранием. // Данный тест будет рабоать только на системах, которые копируют // аргумент putenv()'а во внутренний буфер. То есть перезатирание // нами буфера не приведет к изменению значения установленной // переменной. TEST(PutEnv, UsingStaticVariableRewriteBuffer) { static char buf[1024]; checkPutEnv(buf, true); }
Файл головной программы запуска тестов (runner.cc
):
#include "gtest/gtest.h" int main(int argc, char* argv[]) { testing::InitGoogleTest(&argc, argv); return RUN_ALL_TESTS(); }
Сначала пробуем в Visual Studio.
Компилируем:
cl /EHsc /I. /Feputenv_unittest_vs2008.exe runner.cpp putenv_unittest.cpp gtest\gtest-all.cc
Запускаем putenv_unittest_vs2008.exe’:
[==========] Running 2 tests from 1 test case.
[----------] Global test environment set-up.
[----------] 2 tests from PutEnv
[ RUN ] PutEnv.UsingStaticVariable
[ OK ] PutEnv.UsingStaticVariable
[ RUN ] PutEnv.UsingStaticVariableRewriteBuffer
[ OK ] PutEnv.UsingStaticVariableRewriteBuffer
[----------] Global test environment tear-down
[==========] 2 tests from 1 test case ran.
[ PASSED ] 2 tests.
Видно, что все тесты работают, а это значит, что микрософтовский putenv()
копирует значение аргумента во внутренний буфер, и значит можно передавать в него указатель на нестатический блок памяти.
Теперь пробуем тоже под виндами, но через Cygwin.
Компилируем:
g++ -I. -o putenv_unittest_cygwin.exe putenv_unittest.cpp runner.cpp gtest/gtest-all.cc
Запускаем:
[==========] Running 2 tests from 1 test case.
[----------] Global test environment set-up.
[----------] 2 tests from PutEnv
[ RUN ] PutEnv.UsingStaticVariable
[ OK ] PutEnv.UsingStaticVariable
[ RUN ] PutEnv.UsingStaticVariableRewriteBuffer
putenv_unittest.cpp:15: Failure
Value of: actual
Actual: "novalue"
Expected: std::string("testvalue")
Which is: "testvalue"
[ FAILED ] PutEnv.UsingStaticVariableRewriteBuffer
[----------] Global test environment tear-down
[==========] 2 tests from 1 test case ran.
[ PASSED ] 1 test.
[ FAILED ] 1 test, listed below:
[ FAILED ] PutEnv.UsingStaticVariableRewriteBuffer
1 FAILED TEST
Видно, что тест PutEnv.UsingStaticVariableRewriteBuffer
, ожидая получить значение переменной testvalue
, получил novalue
. Ясно, мы принудительно перезаписали оригинальное значение переменной прямо в буфере после вызова putenv()
. Вывод: Cygwin’овский putenv()
не копирует значение аргумента куда-либо и, значение буфера используется напрямую, поэтому тут надо передавать указатель обязательно на статический блок памяти, или хотя бы такой блок, которые не исчезнет на момент обращения к переменной.
Резюме.
Будьте осторожны при использовании функции putenv()
, так как на некоторых платформах ее аргумент не копируется во внутренний буфер, как бы это ни подсказывала вам очевидная логика, а используется прямо из вашего буфера.
Другие посты по теме:
]]>std::vector
, специализированный для хранения типа bool
, то есть std::vector<bool>
, который по задумке создателей должен работать заметно быстрее своего смыслового аналога std::vector<int>
, на самом деле нет так и хорош. Но тут, как говориться, бабушка на двое сказала, так как с одной стороны операция с базовым типом процессора int
обычно является почти самой быстрой атомарной операцией, а другой стороны тип bool
может быть упакован в тот же “быстрый” int
пачкой по 32 или 64 штуки за раз, и можно оперировать сразу группой значений. В общем, целое поле для оптимизации.
Я люблю все проверять лично, так что привожу результаты своей проверки.
Итак, объект — программа нахождения простых чисел Решето Эратосфена. Классический алгоритм для проверки на вшивость всяких оптимизаторов. На оригинальность и оптимальность кода не претендую.
era.cpp
:
#include <iostream> #include <vector> #include <cmath> int main(int argc, char* argv[]) { // Получаем предельное значение эксперимента из командной // строки. По умолчанию - 100. Это основной, влияющий // на время работы алгоритма, параметр. long n = argc > 1 ? std::atoi(argv[1]) : 100; // Корень квадратный из максимума, округленный до большего // целого. long sqrt_n = static_cast<long>(std::sqrt(static_cast<double>(n))) + 1; // Массив-вектор для хранения значений. Это центр внимания нашего // эксперимента. Макрос TYPE задает тип элементов вектора и должен // быть задан в опциях при компиляции: -DTYPE=int или // -DTYPE=bool соответственно. std::vector<TYPE> S(n, true); // Собственно, решето Эратосфена. for (int i = 2; i < sqrt_n; ++i) if (S[i]) for (int j = i*i; j < n; j += i) S[j] = false; // Подсчет количество найденных простых чисел. Делаем это для // самопроверки. int count = 0; for (int i = 2; i < n; ++i) if (S[i]) count++; // Печатаем найденное количество. std::cout << count << std::endl; return 0; }
Эксперимент я проводил на ноутбуке с процессором Core 2 1ГГц. Для конкретно этой машины я выбрал предел поиска в 10000000. При этом значении времена работы программы с одной стороны небольшие (удобно для повторения замеров), но другой стороны — весьма показательные.
Теперь компилятор. В забеге принимали участие:
Операционная система Windows XP SP3.
Каждый компилятор получил свои максимально полные опции оптимизации на скорость, так как глупо говорить об эффективности программы на С++ без включенной оптимизации компилятора (ни тебе inline-функций, ни использования регистров процессора и т.д.) Но для целостности картины результаты без оптимизации тоже приведены (и будет позже ясно почему).
Для компилирования примера я сделал скрипт, которой компилирует исходную программу каждым компилятором по очереди с использованием типа bool
и int
, с оптимизацией и без. Итого по четыре варианта на каждый компилятор.
build.cmd
:
bcc32 -DTYPE=bool -eera-bcc32-bool.exe era.cpp bcc32 -DTYPE=int -eera-bcc32-int.exe era.cpp bcc32 -O2 -DTYPE=bool -eera-bcc32-bool-opt.exe era.cpp bcc32 -O2 -DTYPE=int -eera-bcc32-int-opt.exe era.cpp g++ -DTYPE=bool -o era-g++-bool.exe era.cpp g++ -DTYPE=int -o era-g++-int.exe era.cpp g++ -O3 -funroll-all-loops -fomit-frame-pointer -mtune=nocona -DTYPE=bool -o era-g++-bool-opt.exe era.cpp g++ -O3 -funroll-all-loops -fomit-frame-pointer -mtune=nocona -DTYPE=int -o era-g++-int-opt.exe era.cpp call cl2008.cmd cl /EHsc /DTYPE=bool /Feera-cl2008-bool.exe era.cpp cl /EHsc /DTYPE=int /Feera-cl2008-int.exe era.cpp cl /EHsc /arch:SSE2 /O2 -DTYPE=bool /Feera-cl2008-bool-opt.exe era.cpp cl /EHsc /arch:SSE2 /O2 -DTYPE=int /Feera-cl2008-int-opt.exe era.cpp call cl2005.cmd cl /EHsc /DTYPE=bool /Feera-cl2005-bool.exe era.cpp cl /EHsc /DTYPE=int /Feera-cl2005-int.exe era.cpp cl /EHsc /arch:SSE2 /O2 -DTYPE=bool /Feera-cl2005-bool-opt.exe era.cpp cl /EHsc /arch:SSE2 /O2 -DTYPE=int /Feera-cl2005-int-opt.exe era.cpp
При скрипты cl2005.cmd
и cl2008.cmd
я уже писал.
После компиляции должны получиться 16 исполняемых файлов с сообразными именами.
Далее, запуск. Для этого можно использовать следующий скрипт (run.cmd
).
ntimer -1 era-cl2005-bool.exe 10000000 ntimer -1 era-cl2005-int.exe 10000000 ntimer -1 era-cl2005-bool-opt.exe 10000000 ntimer -1 era-cl2005-int-opt.exe 10000000 ntimer -1 era-cl2008-bool.exe 10000000 ntimer -1 era-cl2008-int.exe 10000000 ntimer -1 era-cl2008-bool-opt.exe 10000000 ntimer -1 era-cl2008-int-opt.exe 10000000 ntimer -1 era-bcc32-bool.exe 10000000 ntimer -1 era-bcc32-int.exe 10000000 ntimer -1 era-bcc32-bool-opt.exe 10000000 ntimer -1 era-bcc32-int-opt.exe 10000000 ntimer -1 era-g++-bool.exe 10000000 ntimer -1 era-g++-int.exe 10000000 ntimer -1 era-g++-bool-opt.exe 10000000 ntimer -1 era-g++-int-opt.exe 10000000
Для измерения времени работы я использовал программу ntimer. Ее нужно скачать, распаковать и положить ntimer.exe
в текущий каталог. Будучи запущенной с ключом “-1” эта программа печатает времена в одну строку. Нас интересует самое первое печатаемой ей время.
Барабанная дробь! Запускаем…
Таблица с временами работы (по порядку):
Компилятор Версия Тип элемента Оптимизация Время (сек.)
---------------------- ------ ------------ ----------- ------------
Visual Studio 2005 14.00 bool Выкл 23.750
Visual Studio 2005 14.00 int Выкл 1.750
Visual Studio 2005 14.00 bool Вкл 1.171
Visual Studio 2005 14.00 int Вкл 1.312
Visual Studio 2008 15.00 bool Выкл 23.062
Visual Studio 2008 15.00 int Выкл 1.703
Visual Studio 2008 14.00 bool Вкл 2.390
Visual Studio 2008 14.00 int Вкл 1.312
Borland/Codegear 2007 5.93 bool Выкл 8.375
Borland/Codegear 2007 5.93 int Выкл 1.296
Borland/Codegear 2007 5.93 bool Вкл 8.156
Borland/Codegear 2007 5.93 int Вкл 1.328
gcc (cygwin) 3.4.4 bool Выкл 4.640
gcc (cygwin) 3.4.4 int Выкл 3.109
gcc (cygwin) 3.4.4 bool Вкл 0.984
gcc (cygwin) 3.4.4 int Вкл 1.343
А теперь в отсортированном виде по возрастанию времени:
Компилятор Версия Тип элемента Оптимизация Время (сек.)
---------------------- ------ ------------ ----------- ------------
gcc (cygwin) 3.4.4 bool Вкл 0.984
Visual Studio 2005 14.00 bool Вкл 1.171
Borland/Codegear 2007 5.93 int Выкл 1.296
Visual Studio 2005 14.00 int Вкл 1.312
Visual Studio 2008 14.00 int Вкл 1.312
Borland/Codegear 2007 5.93 int Вкл 1.328
gcc (cygwin) 3.4.4 int Вкл 1.343
Visual Studio 2008 15.00 int Выкл 1.703
Visual Studio 2005 14.00 int Выкл 1.750
Visual Studio 2008 14.00 bool Вкл 2.390
gcc (cygwin) 3.4.4 int Выкл 3.109
gcc (cygwin) 3.4.4 bool Выкл 4.640
Borland/Codegear 2007 5.93 bool Вкл 8.156
Borland/Codegear 2007 5.93 bool Выкл 8.375
Visual Studio 2008 15.00 bool Выкл 23.062
Visual Studio 2005 14.00 bool Выкл 23.75
Итак, на первом месте gcc
в режиме bool
с оптимизацией. На втором месте Visual Studio снова в режиме bool
и оптимизацией. Интересно выступил борландовый компилятор, получив третье место, причем без оптимизации. Так как априори борландовый bcc32.exe
считается весьма посредственным компилятором в плане качества кода и оптимизатора, то полученное им третье место весьма и весьма странно.
Конечно, пытливый читатель сразу заметит, что я как-то очень лихо проскочил один очень важный вопрос, а именно — версию STL. Не могу поручиться, что каждый из этих компиляторов поставляется с абсолютно неизменной и, как принято считать, “стандартной” версией этой библиотеки. Каждая фирма что-то меняет всегда под себя.
В итоге, я так и не получил однозначного ответа на изначальный вопрос — пользоваться ли std::vector<int>
вместо std::vector<bool>
или нет. Слишком много побочных факторов. Поэтому я бы посоветовал, если вы встали перед такой же дилеммой в вашем проекте, провести эксперимент на месте с вашим конкретным компилятором, вашей версией STL, на вашей конкретной платформе и т.д., то есть с учетом всех ваших факторов. Можно использовать приведенные мной программы и скрипты. Если у вас будут интересные и неоднозначные результаты, пишите.
time()
конечно хороша своей переносимостью, но она работает с секундами, а хочется что-то более быстрое. Микросекунды - это уже тоже перебор. А вот миллисекунды - самое оно.
Итак, задача: сделать простой и переносимый класс C++ для работы с интервалами времени в миллисекундах. Должно работать в Windows и UNIX.
Я придумал вот такой интерфейс для класса:
class PreciseTimer { public: // Тип для работы с тиками таймера. По сути это целое в 64 бита, // но конкретное имя рабочего типа будет зависеть от платформы. typedef s_int_64 Counter; // Функция получение текущего значения миллисекундного таймера. // Само по себе это число особого смысла не имеет, так как оно // ни к чему не привязано, а вот разница двух таких чисел как // раз используется для замеров интервалов времени. // Функция возвращает 0 под Windows, если не удается получить // значение системного таймера. Counter millisec(); // Задержка на указанное число миллисекунд. Необходимо учитывать, // что в UNIX системах данная функция может быть прервана // системым сигналом (signal). В этом случае задержка может быть // меньше, чем ожидалось. static void sleepMs(int ms); // Функция "отметки" текущего момента времени. // Добавляет текущее время в очередь отметок. void mark(); // Функция получения времени, прошедшего с последней отметки // в функции mark(). Последняя отметка вынимается из очереди // и вычитается из текущего значения таймера. Эта разница и // есть результат функции. Если очередь отметок пуста (никто // не вызывал mark() до этого), то возвращается -1. Counter release(); // Парные вызовы mark()/release() могут быть вложенными. // // Примерная техника работы с классом: // ... // PreTimer timer; // ... // timer.mark(); // ...что-то делаем тут (1) // timer.mark(); // ...что-то еще делаем тут (2) // /* получаем продолжительность дела (2) */ // t1 = timer.release(); // /* получаем суммарную продолжительность дел (1) и (2) */ // t2 = timer.release(); // /* А t3 уже равно -1, так как очередь пуста, так как этот // * вызов release() третий в счету, а вызовов mark() было // * всего два */ // t3 = timer.release(); }
Реалиазация вышла довольно простая. Всего один файл pretimer.h
(без .cpp) без внешних нестандартных зависимостей.
pretimer.h
:
#ifndef EXT_PRETIMER_H #define EXT_PRETIMER_H #include <stack> #ifdef WIN32 #include <windows.h> #else #include <sys/time.h> #include <unistd.h> // usleep() #endif // namespace, традиционно, с именем "ext", так что измените под ваши // привычки именования, если надо. namespace ext { class PreciseTimer { public: #ifdef WIN32 // Тип int64 для Windows typedef LONGLONG Counter; #else // Тип int64 для UNIX typedef long long Counter; #endif PreciseTimer(); Counter millisec(); void mark(); Counter release(); static void sleepMs(int ms); private: // Тип стека для хранения отметок времени. typedef std::stack< Counter > Counters; // Стек для хранения отметок времени. Counters __counters; #ifdef WIN32 // Для Windows надо хранить системную частоту таймера. LARGE_INTEGER __freq; #endif }; void PreciseTimer::mark() { __counters.push(millisec()); } PreciseTimer::Counter PreciseTimer::release() { if( __counters.empty() ) return -1; Counter val = millisec() - __counters.top(); __counters.pop(); return val; } #ifdef WIN32 PreciseTimer::PreciseTimer() { // Для Windows в конструкторе получаем системную частоту таймера // (количество тиков в секунду). if (!QueryPerformanceFrequency(&__freq)) __freq.QuadPart = 0; } PreciseTimer::Counter PreciseTimer::millisec() { LARGE_INTEGER current; if (__freq.QuadPart == 0 || !QueryPerformanceCounter(¤t)) return 0; // Пересчитываем количество системных тиков в миллисекунды. return current.QuadPart / (__freq.QuadPart / 1000); } void PreciseTimer::sleepMs(int ms) { Sleep(ms); } #else // WIN32 PreciseTimer::PreciseTimer() {} PreciseTimer::Counter PreciseTimer::millisec() { struct timeval tv; gettimeofday(&tv, NULL); return tv.tv_sec * 1000 + tv.tv_usec / 1000; } void PreciseTimer::sleepMs(int ms) { usleep(ms * 1000); } #endif // WIN32 } // ext #endif // _EXT_PRETIMER_H
Итак, класс готов, но надо попробовать его в работе. Я, как сугубый апологет unit-тестирования, напишу тесты. Для их компиляции вам потребуется библиотека Google Test Framework. Вы можете взять оригинал с официального сайта, а можете для простоты воспользоваться моей версией, упакованной в два компактных файла gtest-all.cc
и gtest.h
. Я уже писал про это в рассказе про unit-тестирование. Там я подробно описал, как подготовить Google Test к удобной работе.
Итак, тесты.
pretimer_unittest.cpp
:
#include "gtest/gtest.h" #include <cstdlib> // Подключаем наш класс #include "pretimer.h" // Простой тест, для Windows, в основном, для проверки // доступности системного таймера. TEST(PreciseTimer, PreciseSystemTimerAvailability) { ext::PreciseTimer timer; // Если метод millisec() возвращает 0, значит недоступен // системный таймер. EXPECT_NE(0, timer.millisec()) << "Недоступен системный таймер"; } // Тестирует "точность" измерений. TEST(PreciseTimer, MeasurementAccuracy) { // Тестируем на задержке в 100 миллисекунд. const int delay_ms = 100; // Зададим наше допустимое отклонение в 10% (10 миллисекунд). // Функция задержки msleep() тоже неидеальна и привносит // какую-то погрешность помимо наших измерений. const int allowed_delta_ms = 10; // Создаем таймер ext::PreciseTimer timer; // Замечаем время timer.mark(); // Ждем 100 миллисекунд msleep(delay_ms); // Вычисляем модуль разницы между эталоном в 100 миллисекунд // и измеренным нами интервалом через mark()/release() int delta = std::abs(static_cast<int>(delay_ms - timer.release())); // Если отклонение более 10 миллисекунд - ошибка. EXPECT_TRUE(delta <= allowed_delta_ms) << "Слишком большое отклонение " << delta << ", превышающее " << allowed_delta_ms; } // Тестируем очередь замеров TEST(PreciseTimer, Queue) { // Создаем таймер ext::PreciseTimer timer; // Делаем замер номер 1 timer.mark(); // Делаем замер номер 2 timer.mark(); // Получаем текущее значение таймера ext::PreciseTimer::Counter a = timer.release(); // Ждем 100 миллисекунд monitor::PreciseTimer::sleepMs(100); // Проверяем, что значение таймера до задежки // меньше, чем после. Этим мы проверили, что // очередь замеров работает, так как получили // корректное значение второго в очереди замера. EXPECT_LT(a, timer.release()); } // Проверка пустой очередь замеров TEST(PreciseTimer, EmptyQueue) { ext::PreciseTimer timer; // Если очередь замеров пуста, метод release() должен // возвращать -1. EXPECT_EQ(-1, timer.release()); }
Я потратил на этот класс часа четыре неторопливой работы, а на написание тестов всего полчаса, но эти полчаса будут мне служить верой и правдой еще очень долго.
Забавно, что когда я запускал эти тесты как-то на Windows под виртуальной машиной, то тест MeasurementAccuracy давал сбой! Видимо виртуальная машина как-то неправильно эмулировала работу таймеров, и замер делался совершенно неправильно. А вот теперь если представить - как бы я искал этот баг вручную по всей боевой программе, а? Кто ж мог предположить, что в виртуальной среде что-то можно пойти не так с таймерами.
Снова повторю - unit тестирование forever!
В завершении, нам нужна главная программа для запуска тестов:
#include "gtest/gtest.h" int main(int argc, char* argv[]) { // Инициализируем библиотеку testing::InitGoogleTest(&argc, argv); // Запускаем все тесты, прилинкованные к проекту return RUN_ALL_TESTS(); }
Компилируем:
Visual Studio:
cl /EHsc /I. /DWIN32 /Fepretimer_unittest.exe runner.cpp pretimer_unittest.cpp gtest-all.cc
UNIX:
g++ -I. -o pretimer_unittest runner.cpp pretimer_unittest.cpp gtest-all.cc
Запускаем pretimer_unittest
и получаем:
[==========] Running 3 tests from 1 test case.
[----------] Global test environment set-up.
[----------] 4 tests from PreciseTimer
[ RUN ] PreciseTimer.PreciseSystemTimerAvailability
[ OK ] PreciseTimer.PreciseSystemTimerAvailability
[ RUN ] PreciseTimer.MeasurementAccuracy
[ OK ] PreciseTimer.MeasurementAccuracy
[ RUN ] PreciseTimer.Queue
[ OK ] PreciseTimer.Queue
[ RUN ] PreciseTimer.EmptyQueue
[ OK ] PreciseTimer.EmptyQueue
[----------] Global test environment tear-down
[==========] 4 tests from 1 test case ran.
[ PASSED ] 4 tests.
Ура! Все работает. Доказано тестами. При использовании данного класса у себя в проекте не забудьте добавить pretimer_unittest.cpp
в набор ваших прочих unit тестов. Этим вы избавитесь от множества сюрпризов.
Приобщайтесь к unit-тестированию, и программируйте правильно!
]]>cl.exe
, a версий три). В итоге я убрал из путей PATH все ссылки на каталоги разных версии студии, и сделал вот такие скрипты, помещенные в любой каталог, находящийся в списке путей PATH.
Visual Studio 2003, файл: cl2003.cmd
:
@"%VS71COMNTOOLS%\vsvars32.bat"
Visual Studio 2005, файл: cl2005.cmd
:
@"%VS80COMNTOOLS%\vsvars32.bat"
Visual Studio 2008, файл: cl2008.cmd
:
@"%VS90COMNTOOLS%\vsvars32.bat"
Если вы ставили студии по умолчанию стандартным образом, то в системе должны быть переменные окружения VS71COMNTOOLS
, VS80COMNTOOLS
и VS90COMNTOOLS
, задающие расположение конкретной версии. Скрипт же vsvars32.bat
поставляется вместе со студией и автоматически настраивает все необходимое для компилятора окружение.
Теперь компиляция в версии 2005 делается, например, вот таким cmd-файлом:
call cl2005.cmd
cl /Fetest.exe test.cpp
Очевидно, что для перехода на версию 2003 или 2008 надо заменить всего одну цифру. Очень удобно.
]]>Дожив до четвертого десятка и имея за спиной десяток с хвостиком, посвященный программированию, к своему огромному стыду к программированию с использованием блочного тестирования (TDD - test driven development) я приобщился только год назад. Честно могу сказать - это было для меня одним из сильнейших потрясений в профессиональной области за последнее время, и радикально поменяло некоторые фундаментальные представления о разработке софта. Как прирожденный максималист в профессии (за что часто очень нелюбим коллегами по цеху, которые руководствуются правилом “лучшее враг хорошего”), я работаю под девизом “мои программы должны быть безупречны”. А так как тут мне дали в руки такой волшебный инструмент как блочное тестирование, я стараюсь теперь его применять где только возможно. Даже порой радикально перерабатывая старые проекты.
Ладно, это лирика. Приступим к делу.
У нас есть класс Thread, расположенный в файлах thread.cpp
и thread.h
.
Напишем небольшой пример (thread_example.cpp
).
#include <iostream> #include "thread.h" // Создаем наследника от класса Thread class MyThread: public ext::Thread { public: // Инициализируем в false флаг завершения в конструкторе MyThread() : __done(false) {} virtual void Execute() { // В процессе работы потока меняем флаг завершения на истину __done = true; } // Функция, возвращающая значение флага завершение bool done() const { return __done; } private: bool __done; }; int main(int argc, char* argv[]) { // Создаем объект потока. Пока он еще не запущен. MyThread thread; // Печатаем значение флага завершения. Должно быть 0 (false) std::cout << "Thread status before: " << thread.done() << std::endl; // Запускаем поток thread.Start(); // И ждем его завершения thread.Join(); // Если поток нормально был запущен и отработал, то значение // флага должно измениться на 1 (true). Это должна сделать // функция Execute(). Если тут будет не 1, а 0, значит поток // не выполнялся, и выходит, что с классом что-то не так. std::cout << "Thread status after: " << thread.done() << std::endl; }
Компилируем (естественно, из командной строки).
Visual Studio 2008 (хотя подойдет любая версия VS):
cl /EHsc /I. /Fethread_example /DWIN32 thread_example.cpp thread.cpp
Опция /EHsc
нужна, так как мы пишем на С++, и поэтому компилятору cl.exe
надо явно указать необходимость включения поддержки исключений. Особенность данного компилятора.
Если вы в UNIX’e, тогда, например, gcc
:
g++ -o thread_example thread_example.cpp thread.cpp
Запускаем thread_example, и имеем на экране следующее:
Thread status before: 0
Thread status after: 1
Судя по напечатанным данным, класс работает правильно.
Я специально не использовал в функции Execute()
отладочной печати на экран типа “Hello, world! I’m the thread”. Хотя это было бы нагляднее и прикольнее, чем какие-то булевы флаги. Но на это была причина. При работе с потоками, когда ваш код теперь уже выполняется нелинейно, а какие-то фрагменты могут работать параллельно, приходится очень тщательно продумывать совместное использование переменных одновременно работающими потоками. Может так случиться, что когда основной поток будет печатать что-то на экран через переменную std::cout
, параллельный поток тоже захочет это сделать, прервет основной поток на полпути и сам начнет использовать std::cout
. Данные обоих потоков смешаются, и в лучшем случае на экран вылезет каша, а в худшем программа может завершиться аварийно. На том же мной так любимом AIX’е именно это и происходит. Видимо, стандартная библиотека AIX’а требует каких-то дополнительных настроек для нормальной работы в мультипотоковой среде. Для избежания подобных проблем совместного доступа применяются различные механизмы из мира параллельного программирования - блокировки (mutex), семафоры, критические секции и т.д. Я посвящу отдельный пост этому очень непростому вопросу, но расскажу о нем крайне просто и понятно.
Теперь давайте запустим десяток потоков (thread_example2.cpp
).
#include <vector> #include <iostream> #include "thread.h" class MyThread: public ext::Thread { public: MyThread(int id) : __id(id), __done(false) {} virtual void Execute() { // Небольшая "перчинка" программы, чтобы не было скучно. // Суть в том, что поток с индексом 3 (по счету номер 4, так первый // индекс 0) не будет устанавливать флаг выполнения. Сделано это // просто для разнообразия. Результат данной "перчинки" будет виден // при печати. if (__id != 3) __done = true; } bool done() const { return __done; } private: int __id; bool __done; }; typedef std::vector<MyThread*> Threads; int main(int argc, char* argv[]) { // Создаем вектор из указателей на потоки std::vector<MyThread*> threads; // Создаем 10 потоков и сохраняем указатели на них в вектор for (int i = 0; i < 10; i++) threads.push_back(new MyThread(i)); // Запускаем потоки на выполнение for (Threads::iterator i = threads.begin(); i != threads.end(); i++) (*i)->Start(); // Дожидаемся, пока они все завершатся for (Threads::iterator i = threads.begin(); i != threads.end(); i++) (*i)->Join(); // Печатаем статусы потоков в одну строку через пробел for (Threads::iterator i = threads.begin(); i != threads.end(); i++) std::cout << (*i)->done() << " "; std::cout << std::endl; // Чистим за собой память. for (Threads::iterator i = threads.begin(); i != threads.end(); i++) delete *i; }
Компилируем.
Visual Studio:
cl /EHsc /I. /Fethread_example2 /DWIN32 thread_example2.cpp thread.cpp
В UNIX’e (gcc
):
g++ -o thread_example2 thread_example2.cpp thread.cpp
Запускаем thread_example2
, и имеем на экране следующее:
1 1 1 0 1 1 1 1 1 1
Видно, что все потоки, кроме четвертого (индекс 3, так как считаем от нуля) установили свои флаги правильно. Четвертому помешала “перчинка” (см. выше).
Что дальше? Да ничего, собственно. Теперь вы наверняка набросаете несколько своих примеров, поиграетесь, и может начнете включать данный класс в свои проекты. Тестовые примеры вы скорее всего сотрете как отработанный материал, а может и заначите до лучших времен.
А теперь! На сцену приглашается unit тестирование.
Я вам предлагаю сделать небольшие программы-тесты, которые бы своими результатами доказывали правильность работы нашего класса. Например:
class SimpleThread: public ext::Thread { public: SimpleThread() : __done(false) {} virtual void Execute() { __done = true; } bool done() const { return __done; } private: bool __done; };
Класс SimpleThread
очень похож на класс MyThread
из наших примеров выше. Он просто меняет флаг активности с false
на true
в процессе успешного выполнения.
// Декларируем тест с именем RunningInParallel в группе тестов ThreadTest. TEST(ThreadTest, RunningInParallel) { // Создаем объект нашего класса SimpleThread thread; // Внимание! Макрос EXPECT_FALSE смотрит, какое значение у его аргумента. // Если это ложь, то все нормально, и выполнение теста идет дальше. Если же нет, // то печатается сообщение об ошибке, хотя тест продолжает работу. // В нашем случае тут должно быть false по смыслу. EXPECT_FALSE(thread.done()); // Запускаем поток на выполнение thread.Start(); // Ждем завершение потока thread.Join(); // Макрос EXPECT_TRUE смотрит, какое значение у его аргумента. // Если это истина, то все нормально, и выполнение теста идет дальше. Если же нет, // то печатается сообщение об ошибке, хотя тест продолжает работу. // Тут мы уже ждем не false, а true, потому что поток должен был изменить значение // этого флага. EXPECT_TRUE(thread.done()); }
Теперь осознаем произошедшее - мы не просто написали какой-то пример, а мы формально опередили логику работы класса, задали его ответственность. Теперь наши пожелания к функциональности класса заданы не на словах и предположениях, а в виде программы.
Теперь осталось только запустить этот тест.
Существует много библиотек для unit тестирования практически для каждого языка. С++ не исключение. Самой распространенной в мире С++ является CppUnit. Но около полугода назад Google ворвался в мир библиотек тестирования с Google Test Framework. На момент написания данной статьи актуальной версией является 1.2.1. Распространяется в исходных текстах. Данную библиотеку можно прекомпилировать и использовать как двоичный модуль при линковке, но я сделал иначе. Так как я постоянно прыгаю с платформы на платформу, с компилятора на компилятор, мне удобнее компилировать Google Test прямо из исходников каждый раз при сборке проекта, благо библиотека хорошо портируема, мала по размеру и быстро компилируется. К небольшому сожалению, Google Test реализована в виде не одного файла-исходника и одного .h файла, а целого набора .h файлов и набора .cc (.cpp) файлов. Так удобно библиотеку развивать (что логично), но не использовать из исходников со стороны. Поэтому я объединил всю библиотеку в два файла: gtest.h
и gtest-all.cc
, и больше ничего не нужно. Гугловцы обещали в следующий релиз библиотеки включить мой патч на эту тему. Сейчас же они (также по моей идее) дают специальный скрипт, которым можно из официального архива сделать компактную версию из двух файлов. Для тех, у кого уже съехали мозги от этих подробностей, и кто пока не хочет тратить время на техдетали библиотеки, я могу предложить мою сборку Google Test’а. Можно начать с нее. Она основана на официальной версии 1.2.1 и является объединением множества файлов в два. В архиве два файла gtest/gtest.h
и gtest-all.cc
. Положите их в каталог, где будете проводить опыты.
Итак, предположим, вы имеете файлы: gtest/gtest.h
и gtest-all.cc
в рабочем каталоге, и все готово к запуску.
Полный вариант исходника thread_unittest.cpp
:
#include "gtest/gtest.h" #include "thread.h" class SimpleThread: public ext::Thread { public: SimpleThread() : __done(false) {} virtual void Execute() { __done = true; } bool done() const { return __done; } private: bool __done; }; TEST(ThreadTest, RunningInParallel) { SimpleThread thread; EXPECT_FALSE(thread.done()); thread.Start(); thread.Join(); EXPECT_TRUE(thread.done()); }
Я предпочитаю давать имена файлам с тестами, используя суффикс _unittest
к имени основного файла. Это позволяет, быстро взглянув на каталог, понять - какие классы имеют тесты, а какие нет.
Также нам нужен стартовый файл runner.cpp
, который будет содержать функцию main()
:
#include "gtest/gtest.h" int main(int argc, char* argv[]) { // Инициализируем библиотеку testing::InitGoogleTest(&argc, argv); // Запускаем все тесты, прилинкованные к проекту return RUN_ALL_TESTS(); }
Тут все просто. Обычно, этот файл одинаков для все тестовых проектов, если вам не надо проводить какие-нибудь дополнительные инициализации, брать что-то из командной строки и.д. Google Test устроена так (в отличие от CppUnit, например), что тесты (TEST
и TEST_F
) не надо нигде дополнительно регистрировать, объявлять и т.д. Вы просто задаете тело теста, включаете файл с исходником в проект и все. Далее все происходит автоматически.
Резонный вопрос - а в каком порядке тесты буду выполнены, если их несколько? А ответ прост: вас это не касается. Тесты могут выполняться в любом порядке, и нельзя делать никаких предположений на эту тему. Суть тут в том, что каждый тест должет быть атомарным и независимым (конечным автоматом без памяти). В этом суть блочного (unit) тестирования, когда маленькие кусочки большой программы проверяются отдельно, в полной изоляции. Но, вернемся к компиляции.
Компилируем.
Visual Studio:
cl /EHsc /DWIN32 /I. /Fethread_unittest.exe runner.cpp thread_unittest.cpp thread.cpp gtest-all.cc
UNIX:
g++ -I. -o thread_unittest runner.cpp thread_unittest.cpp thread.cpp gtest-all.cc
Запускаем thread_unittest
и получаем что-то вроде:
[==========] Running 1 tests from 1 test case.
[----------] Global test environment set-up.
[----------] 1 tests from ThreadTest
[ RUN ] ThreadTest.RunningInParallel
[ OK ] ThreadTest.RunningInParallel
[----------] Global test environment tear-down
[==========] 1 tests from 1 test case ran.
[ PASSED ] 1 tests.
Это значит, что тест был запущен и отработал как положено.
Добавим еще один тест, который будет проверять, убивается ли поток, когда мы этого хотим.
thread_unittest.cpp
:
#include "gtest/gtest.h" #include "thread.h" #ifdef WIN32 #include <windows.h> #define msleep(x) Sleep(x) #else #include <unistd.h> #define msleep(x) usleep((x)*1000) #endif class SimpleThread: public ext::Thread { public: SimpleThread() : __done(false) {} virtual void Execute() { __done = true; } bool done() const { return __done; } private: bool __done; }; TEST(ThreadTest, RunningInParallel) { SimpleThread thread; EXPECT_FALSE(thread.done()); thread.Start(); thread.Join(); EXPECT_TRUE(thread.done()); } // "Нескончаемый поток" class GreedyThread: public ext::Thread { public: virtual void Execute() { // Данный поток будет работать вечно, пока его не убьют извне. while (true) { msleep(1); } } }; TEST(ThreadTest, Kill) { // Создаем "вечный" поток GreedyThread thread; // Запускаем его thread.Start(); // Убиваем его thread.Kill(); // Если функция Kill() не работает, ты мы никогда не дождемся окончания потока // и программа тут повиснет. thread.Join(); }
Компилируем.
Visual Studio:
cl /EHsc /I. /Fethread_unittest.exe /DWIN32 runner.cpp thread_unittest.cpp thread.cpp gtest-all.cc
UNIX:
g++ -I. -o thread_unittest runner.cpp thread_unittest.cpp thread.cpp gtest-all.cc
Запускаем thread_unittest
и получает что-то вроде:
[==========] Running 2 tests from 1 test case.
[----------] Global test environment set-up.
[----------] 2 tests from ThreadTest
[ RUN ] ThreadTest.RunningInParallel
[ OK ] ThreadTest.RunningInParallel
[ RUN ] ThreadTest.Kill
[ OK ] ThreadTest.Kill
[----------] Global test environment tear-down
[==========] 2 tests from 1 test case ran.
[ PASSED ] 2 tests.
Оба теста отработали правильно. Получается, что теперь мы точно уверены, что наш поток умеет работать параллельно и независимо от основного потока, и умеет принудительно “убиваться” по требованию. Мы это доказали тестами, а не словами или алгоритмами на бумаге. Если вам кажется, что еще не вся функциональность класса проверена, обязательно допишите свои тесты для проверки своих предположений.
Теперь внесем в класс “случайную ошибку”, добавив оператор return
в виндовый вариант функции void Thread::Start()
:
void Thread::Start() { // "Случайная" ошибка return; __handle = CreateThread( 0, 0, reinterpret_cast<LPTHREAD_START_ROUTINE>(ThreadCallback), this, 0, 0 ); }
Теперь наш класс “сломан”. Посмотрим, что скажет тестирование (естественно, надо перекомпилировать программу перед этим):
[==========] Running 2 tests from 1 test case.
[----------] Global test environment set-up.
[----------] 2 tests from ThreadTest
[ RUN ] ThreadTest.RunningInParallel
thread_unittest.cpp(33): error: Value of: thread.done()
Actual: false
Expected: true
[ FAILED ] ThreadTest.RunningInParallel
[ RUN ] ThreadTest.Kill
[ OK ] ThreadTest.Kill
[----------] Global test environment tear-down
[==========] 2 tests from 1 test case ran.
[ PASSED ] 1 test.
[ FAILED ] 1 test, listed below:
[ FAILED ] ThreadTest.RunningInParallel
1 FAILED TEST
Бинго! Тест говорит, что ожидаемое значение флага выполнения “истина”, а реальное “ложь”. Класс не работает! Конечно не работает, так как создание потока не происходит из-за “случайного” оператора return
. Мы нашли реальный “баг”, причем сделали это автоматизированным образом.
Можно еще улучшить тест дополнительной информацией, которая будет показана в случае его сбоя:
TEST(ThreadTest, Simple) { SimpleThread thread; EXPECT_FALSE(thread.done()); thread.Start(); thread.Join(); EXPECT_TRUE(thread.done()) << "Поток не изменил флаг"; }
Теперь сообщение об ошибке будет более информативно.
[==========] Running 2 tests from 1 test case.
[----------] Global test environment set-up.
[----------] 2 tests from ThreadTest
[ RUN ] ThreadTest.RunningInParallel
thread_unittest.cpp(33): error: Value of: thread.done()
Actual: false
Expected: true
Поток не изменил флаг
[ FAILED ] ThreadTest.RunningInParallel
[ RUN ] ThreadTest.Kill
[ OK ] ThreadTest.Kill
[----------] Global test environment tear-down
[==========] 2 tests from 1 test case ran.
[ PASSED ] 1 test.
[ FAILED ] 1 test, listed below:
[ FAILED ] ThreadTest.RunningInParallel
1 FAILED TEST
Google Test имеет множество функций для тестовых сравнений, но основные их них, используемые в 99% случаев, следующие:
EXPECT_EQ(a, b)
- проверка условия “a = b”EXPECT_NE(a, b)
- проверка условия “a != b”EXPECT_GT(a, b)
- проверка условия “a > b”EXPECT_LT(a, b)
- проверка условия “a < b”EXPECT_GE(a, b)
- проверка условия “a >= b”EXPECT_LE(a, b)
- проверка условия “a <= b”EXPECT_TRUE(a)
- проверка аргумента на истинуEXPECT_FALSE(a)
- проверка аргумента на ложьФункции, начинающиеся с EXPECT_
, в случае ошибки не прерывают выполнение теста, а просто печатают сообщение об ошибке, и тестирование продолжается. Если ваша ошибка фатальна (например, база данных недоступна), и нет причин продолжать тесты вообще, то можно использовать функции со схожим именованием:
ASSERT_EQ(a, b)
- проверка условия “a = b”ASSERT_NE(a, b)
- проверка условия “a != b”ASSERT_GT(a, b)
- проверка условия “a > b”ASSERT_LT(a, b)
- проверка условия “a < b”ASSERT_GE(a, b)
- проверка условия “a >= b”ASSERT_LE(a, b)
- проверка условия “a <= b”ASSERT_TRUE(a)
- проверка аргумента на истинуASSERT_FALSE(a)
- проверка аргумента на ложьЭти фунции при ошибке прерывают тест и весь процесс тестирования с целом.
Есть еще особая функция FAIL()
, которая безусловно прерывает тест с ошибкой. Удобно для проверки мест, где вы “не должны” оказаться в процесса работы теста. Например:
try { ... } catch(...) { FAIL() << "Данный кусок программы не должен генерировать исключений"; }
Полный список функций-проверок, а также описания прочих возможностей Google Test, так как я затронул пока лишь малую их часть, можно получить в документации.
Кроме того, во все эти функции можно писать как стандартные потоки вывода через оператор <<
, как мы делали в примере выше:
EXPECT_TRUE(thread.done()) << "Поток не изменил флаг";
тем самым печатая удобную отладочную информацию.
Давайте проанализируем сказанное и сделанное. Что мы получили? Как я уже говорил, мы формализовали наши требования от класса в виде программы, которую можно теперь запускать сколько угодно раз, проверяя работу класса. Вы спросите для чего? Класс-то работает. А вот представьте, что вы установили новую версию компилятора или новую версию библиотеки pthread
и что-то в этом роде. Вы уверены, что в них нет ошибок? или может нужны другие опции командной строки для правильной работы. Кто знает?! Тест знает! Скомпилированный и запущенный тест сразу же проверит, работает ли класс так, как вы от него ожидаете. По крайне мере хуже уже не будет. Новые ошибки тест может и не покажет, но уже формализованное ранее поведение класса проверит точно. А теперь представьте, что вам надо так перепроверить сотни классов в вашем проекте. Только автоматизированное тестирование делает это реальным. А тестирование типа “давай поерзаем программой быстренько, и если сразу не сломалось, то все хорошо” тестированием не является вообще. Гораздо проще включить компилирование и запуск тестов при каждой полной сборке проекта. Небольшая потеря времени конечно есть на дополнительную компиляцию, но это с лихвой окупается выявленными тут же ошибками. Сами unit тесты обычно работают очень быстро. Они должны быть быстрыми, иначе они неудобны для регулярного запуска. Сотни тестов не должны как-либо заметно медленно работать. Если какой-то тест требует секунд для себя, то может его стоит перенести в раздел функционального тестирования и пользоваться им уже в процессе проверки программы для релиза, а не в процессе самой разработки, или запускать медленные тесты автоматически в ночных сборках.
Кстати, наличие тестов позволяет поручить возможные доработки кода не только тому, кто этот код писал изначально и понимает в самых деталях, как все работает. Если тесты работают, значит изменения кода по крайне мере не сделали его хуже, а значит клиент не будет кричать сразу после установки новой версии типа “какого вы тут все сломали”. Тесты - это прежде доказательства программиста, что его программа работает так, как он ожидает и всем обещает, как его программа должна работать. Только это уже не просто слова, а автоматизированный метод проверки.
Помните те примеры, которые мы писали в начале. Что с ними случилось? Мы их просто выкинули как отработанный материал. Выкинули результаты очень полезной работы. Мы по кусочкам разобрались, как работает исследуемый класс, но потом отказались повторно использовать уже полученные результаты, выкинув тестовые примеры. Так почему бы изначально не приложить чуть-чуть усилий и не оформить тестовые игрушечные примеры в виде блочных тестов, готовых к автоматизированному повторному использованию, и не превратить их в мощное автоматизированное оружие против багов?
Личный пример. Писал я класс, реализующий TCP/IP сокет с использованием SSL. Скачал библиотеку OpenSSL, начал разбираться. Стал писать мини примеры для освоения разных аспектов библиотеки. И каждый свой эксперимент я оформлял в виде теста (один тест для создания контекста ключей, другой для установления соединения, третий для расшифрации кодов ошибок и т.д.). Каждый новый запуск проекта влючал все больше и больше таких тестов. Затем я вынужден был прерваться на месяц. По прошествии месяца я напрочь забыл все про OpenSSL. И если бы не готовые уже тесты, я бы начал разбираться опять сначала. А так, поглядев на уже сделанные куски, я быстро погонял тесты, вспомнил что к чему, и продолжил работу. Затем из этих тестов фактически и родилась моя библиотека для работы с SSL, и сами тесты включились в тестирующую сборку. Когда осваиваешь что-то новое - язык, библиотеку и т.д. - тестовая программа очень быстро разрастается и превращается некоего монстра, в котором вы хотите задействовать и проверить все новое. Гораздо полезнее разбираться по маленьким кусочкам, изолированно изучать каждый вопрос, закрепляя полученные результаты в виде тестов.
Вы меня сходу спросите - а как писать тесты? Ведь данный пример весьма тривиален, а реальные программы гораздо сложнее, в них много взаимозависимостей, и порой крайне сложно раскроить их на тестируемые кусочки. Ответ, который я дам сходу сейчас таков - пишите ваши программы сразу пригодными для тестирования. А вот как именно это делать - я расскажу в будущих выпусках нашего научно-популярного журнала.
А вы меня опять спросите - а другие языки как? как, например, делать unit тестирование в классическом языке С? Об этом я тоже непременно расскажу.
Unit-тестирование — это громадная и очень интересная тема. Будем ее развивать.
P.S. Исходные тексты данной статьи я проверял на Windows, Linux 2.6 (32- и 64-бит Intel и SPARC), AIX 5.3 и 6, SunOS 5.2 64-bit SPARC.
Другие посты по теме:
]]>class Thread { public: ... private: ... // Защита от случайного копирования объекта в C++ Thread(const Thread&); void operator=(const Thread&); };
Это определения конструктора копирования и перегруженого оператора присваивания. Причем непосредственно реализаций этих функций нигде нет, только определения. Вопрос: для чего все это?
Давайте разберемся с назначением этих функций. Их прямая задача уметь копировать объект данного класса. А что произойдет, если вы по какой-то причине не определили конструктор копирования или оператор присваивания (может просто забыли), а пользователь вашего класса решил скопировать объект, возможно даже неосознанно? В этом случае компилятор сам определит конструктор копирования по умолчанию, который будет тупо копировать объект байт за байтом без учета смысла копируемых данных. И вам крупно повезет, если все члены-данные вашего класса являются либо базовыми типами (int
, long
, char
и т.д.), либо имеют корректные конструкторы копирования и операторы присваивания. В этом случае все будет хорошо — базовые типы компилятор умеет копировать правильно, а сложные типы скопируют себя сами через их конструкторы копирования. А представьте, что вы внутри своего класса создаете объекты динамически в куче и храните в классе только указатели на них. Указатеть — это базовый тип, и компилятор его нормально скопирует. А вот данные, на которые этот указатель указывает он копировать не будет. В итоге два объекта (старый-оригинал и новый-копия) будут ссылаться на один кусок памяти в куче. Теперь понятно, что при попытке освобождения этой памяти в деструкторе (если вы не забыли этого сделать) кто-то из этих двух объектов попытается освободить уже освобожденную память. Вероятность аварийного завершения программы в этом случае крайне высока, а поиск подобных ошибок может быть крайне долгим и мучительным.
Отсюда мораль: если для вашего класса не заданы конструктор копирования и оператор присваивания (они вам не нужны по смыслу), сделайте им пустые объявления в разделе закрытом разделе (private
). Тогда попытка скопировать этот объект сразу приведет к ошибке при компиляции. Во-первых, объявления являются закрытыми (private
), и сторонний пользователь вашего класса получит ошибку доступа к закрытым данным класса. Во-вторых, у этих функций нет тел, а значит вы сами не выстрелите себе в ногу, попытавшись случайно скопировать объект данного класса в нем же самом (тут вам private
уже не помеха), если вы на это не рассчитывали при проектирование класса.
Лично я делаю так. У меня есть следующий файл (ctorguard.h
):
#ifndef _EXT_CTOR_GUARD_H #define _EXT_CTOR_GUARD_H #define DISALLOW_COPY_AND_ASSIGN(TypeName) \ TypeName(const TypeName&); \ void operator=(const TypeName&) #endif Теперь определение класса будет выглядеть так: #include "ctorguard.h" class Thread { public: ... private: ... // Защита от случайного копирования объекта в C++ DISALLOW_COPY_AND_ASSIGN(Thread); };
Теперь вы надежно предохранены.
Кстати, вдогонку. При реализации оператора присваивания надо обязательно проверять — не пытаетесь ли вы копировать объект сам в себя, то есть, не является ли источник копирования самими объектом куда идет копирование. Если это произойдет, вы легко можете получить переполнения стека как самый вероятный исход из-за бесконечного вызова оператора присваивания себя самим.
]]>Итак, задался я целью иметь удобный и простой класс на С++ для работы с потоками. В силу особенностей работы мне приходится иметь дело различными системами, и хотелось иметь максимально переносимый вариант. На сегодняшний день стандартом де-факто для мира UNIX являются так называемые потоки POSIX Для Windows тоже есть реализация этой библиотеки, но в целях исключения дополнительной внешней зависимости для этой платформы я решил пользоваться напрямую Windows API, благо назначения функций очень похожи. При использования POSIX Threads под Windows данный класс еще упрощается (надо просто выкинуть всю Windows секцию), но для меня лично удобнее было не иметь зависимости от виндусовых POSIX Threads. Дополнительная гибкость, так сказать.
Исходники приведены прямо тут, благо они небольшие. Комментариев мало, так как я считаю, что лучший комментарий, это грамотно написанный код. Сердце всего дизайна класса — это виртуальный метод void Execute()
, который и реализует работу потока. Данный метод должен быть определен в вашем классе потока, который наследуется от класса Thread.
Я всегда использую пространства имен (namespaces) в C++, особенно для библиотечных классов общего назначения. Для данного примера я использовал имя ext
. Замените его на ваше, если необходимо “вписать” класс в ваш проект.
Для компиляции в Windows необходимо определить макрос WIN32. В этом случае будет использоваться Windows API. Иначе подразумевается работа с pthreads. Если вы используете Cygwin, то можно работать и через Windows API и через pthreads.
thread.h
:
#ifndef _EXT_THREAD_H #define _EXT_THREAD_H #ifdef WIN32 #include <windows.h> #else #include <pthread.h> #include <signal.h> #endif namespace ext { #ifdef WIN32 typedef HANDLE ThreadType; #else typedef pthread_t ThreadType; #endif class Thread { public: Thread() {} virtual ~Thread(); // Функция запуска потока. Ее нельзя совместить с конструктором // класса, так как может случиться, что поток запустится до того, // как объект будет полностью сформирован. А это может спокойно // произойти, если вызвать pthread_create или CreateThread в // в конструкторе. А вызов виртуальной функции в конструкторе, // да еще и в конструкторе недосформированного объекта, // в лучшем случае приведет к фатальной ошибке вызова чисто // виртуальной функции, либо в худшем случае падению программы // с нарушением защиты памяти. Запуск же потока после работы // конструктора избавляет от этих проблем. void Start(); // Главная функция потока, реализующая работу потока. // Поток завершается, когда эта функция заканчивает работу. // Крайне рекомендуется ловить ВСЕ исключения в данной функции // через try-catch(...). Возникновение неловимого никем // исключения приведет к молчаливому падению программы без // возможности объяснить причину. virtual void Execute() = 0; // Присоединение к потоку. // Данная функция вернет управление только когда поток // завершит работу. Применяется при синхронизации потоков, // если надо отследить завершение потока. void Join(); // Уничтожение потока. // Принудительно уничтожает поток извне. Данный способ // завершения потока является крайне нерекомендуемым. // Правильнее завершать поток логически, предусмотрев // в функции Execute() условие для выхода, так самым // обеспечив потоку нормальное завершение. void Kill(); private: ThreadType __handle; // Защита от случайного копирования объекта в C++ Thread(const Thread&); void operator=(const Thread&); }; } // ext #endif
thread.cpp
:
#include "thread.h" namespace ext { static void ThreadCallback(Thread* who) { #ifndef WIN32 // Далаем поток "убиваемым" через pthread_cancel. int old_thread_type; pthread_setcanceltype(PTHREAD_CANCEL_ASYNCHRONOUS, &old_thread_type); #endif who->Execute(); } #ifdef WIN32 Thread::~Thread() { CloseHandle(__handle); } void Thread::Start() { __handle = CreateThread( 0, 0, reinterpret_cast<LPTHREAD_START_ROUTINE>(ThreadCallback), this, 0, 0 ); } void Thread::Join() { WaitForSingleObject(__handle, INFINITE); } void Thread::Kill() { TerminateThread(__handle, 0); } #else Thread::~Thread() { } extern "C" typedef void *(*pthread_callback)(void *); void Thread::Start() { pthread_create( &__handle, 0, reinterpret_cast<pthread_callback>(ThreadCallback), this ); } void Thread::Join() { pthread_join(__handle, 0); } void Thread::Kill() { pthread_cancel(__handle); } #endif } // ext
Возникает резонный вопрос — я почему ни один из вызовов функций не проверяет код ошибки. Вдруг что? Я могу сказать, что я встретил только один случай возврата ошибки от pthread_create.
Это было на AIX’e при использовании связывания (linking) времени исполнения. Программа не была слинкована с библиотекой pthreads (я забыл указать ключик “-lpthread”), но из-за особенностей линковки времени исполнения (так любимой AIX’ом) линкер сообщил, что все хорошо и выдал мне исполняемый файл. В процессе же работы ни одна функция из библиотеки pthreads просто не вызывалась. Интересно, что код ошибки функции pthread_create()
означал что-то типа “не могу открыть файл”, и чего я сделал вывод, что файл библиотеки недоступен. Вообще, линковка времени исполнения — это довольно хитрая штука. В данном виде связывания внешние связи определены уже на стадии линковки (то есть это не тоже самое, что загрузка разделяемой библиотеки вручную во время работы, самостоятельный поиск функций по именам и т.д.), но вот фактический поиск вызываемой функции происходит в сам момент старта программы. Получается, что до непосредственно запуска нельзя проверить в порядке ли внешние зависимости (команда ldd
рапортует, что все хорошо). Более того, разрешение внешних зависимостей происходить в момент вызовы внешней функции. Это довольно гибкий механизм, но вот его практическая полезность пока остается для меня загадкой. Вообще AIX является довольно изощренной системой в плане разнообразия механизмов связывания. Позже я постараюсь описать результаты моих “исследований” AIXа на эту тему.
Но вернемся к причинам отсутствия проверки кодов возврата от функций pthreads и Windows API. Как я уже упомянул, если какая-то из этих функций завешается с ошибкой, то с огромной вероятностью что-то радикально не так в системе, и это не просто нормальное завершение функции с ошибкой, после которой можно как-то работать дальше. Это фатальная ошибка, и ваше приложение не будет работать нормально еще по туче других причин. Кроме этого я хотел сделать это класс максимально простым, чтобы его можно было таскать из проекта в проект и не допиливать его напильником под существующую в проекте систему обработки ошибок (исключения, коды возврата, журналирование и т.д.), так как в каждом проекте она может быть разная.
Читатель всегда может добавить в код необходимые проверки для собственных нужд.
Кроме этого, я всегда использую в разработке unit-тестирование, и данный класс также имеет тесты. Поэтому запускаемый при каждой полной сборке проекта набор тестов сразу выявляет большинство проблем (уже проблемы линковки точно).
В следующей главе я расскажу про технику использования описанного класса — как создавать потоки, как их запускать, останавливать и уничтожать. Я буду использовать unit-тестирование, что позволит все мини-примеры превращать в автоматизированные тесты вашего проекта.
В завершении могу сказать, что данный класс успешно работает и проверен мной лично на Windows (32- и 64-бит), Linux 2.6 (32- и 64-бит Intel и SPARC), AIX 5.3 и 6, SunOS 5.2 64-bit SPARC, HP-UX и HP-UX IA64.
Другие посты по теме:
]]>Математик любит искать во всем логику, закономерности, разумность. Если ее не хватает в реальной жизни, то компьютер, операционная система, языки программирования дополняют этот дефицит, служат своего рода отдушиной, тем сказочным миром, который помогает легче переносить уродливость мира реального. Следовательно, чем более иррациональным будет повседневное бытие, тем больше будет тяга к компьютеру, к его удивительно логичному и разумному поведению, осмысленным действиям, внутренней логике, виртуальной действительности. Там нет тупых и невежественных генералов, очередей за мясом, совхозов и овощебаз, общественной работы и субботников, там только четкие и понятные критерии, TRUE и FALSE, единица и ноль. А специалист по компьютерам, хороший программист всегда будет востребован, при любом правителе, любой идеологии, любых начальниках. Отсюда уже недалеко и до свободы, до реальной свободы, когда пропадает этот инстинктивный трепет перед важными надутыми начальниками, не освоившими толком даже компьютерных игр.
Михаил Масленников Криптография и свобода
Это, наверное, первый и последний нетехнический пост в данном блоге. Далее все будет носить исключительно технический характер про компьютеры, программирование, роботов и так далее со всеми остановками. Изначальная идея была просто сделать сборник моих собственных фишек и штучек, изобретенных или подсмотренных. Многие вещи быстро забываются, и хочется иметь онлайновый справочник. Кроме того, идеи глупо держать под подушкой, там они гниют и тухнут. Идеями надо делиться, что я и планирую делать тут.
Будет интересно. Следите за анонсами.
]]>