最近,計算機系統老師佈置了道作業: 給定一個16M大小的文本文件,裏面是隨機的大小寫混合的字符,要求將所有的字符改爲小寫。
我的電腦硬件環境:
華碩筆記本h550jv,cpu爲i7-4700hq,內存和顯卡就不寫了~應該對我們的性能優化之路影響很低。
原材料已生成好,爲了最大化的測試,我已經準備了16kb-1Gb的測試文件。
先上作業附帶的示例代碼,示例代碼肯定只是一個功能展示,性能可想而知:
// lowercase.cpp : 定義控制檯應用程序的入口點。
//
#include "stdafx.h"
#include <iostream>
#include <Windows.h>
/*待完成的字符串轉換函數,
_instr字符串既是輸入參數也是輸出參數,不再開闢新的內存;
_len爲字符串長度
嘗試不同的文件大小16k,1M,16M或者更大(可使用wstr程序生成更大的文件),
WARNING:大文本文件不要試圖用記事本打開,打開方法google一下
*/
int TransLower(char *_instr, int _len)
{
int i;
for(i=0;i<strlen(_instr);i++)
if(_instr[i]>='A'&&_instr[i]<='Z')
_instr[i]-=('A'-'a');
return 0;
}
int _tmain(int argc, _TCHAR* argv[])
{
//打開文件
HANDLE hfile = CreateFile(L"d:\\char16M.txt", GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
int fileSize = 0;
if (INVALID_HANDLE_VALUE == hfile)
{
return -1;
}
else
{
fileSize = GetFileSize(hfile, NULL);
std::cout << "the size of file : " << fileSize << std::endl;
}
//開闢內存
char *charBuf = NULL;
try
{
charBuf = new char [fileSize];
}
catch (std::bad_alloc& e)
{
std::cout << "內存申請失敗" << std::endl;
return -2;
}
//將文件中的字符載入到內存中
DWORD dwRet;
ReadFile(hfile, charBuf, fileSize, &dwRet, NULL);
CloseHandle(hfile);
hfile = INVALID_HANDLE_VALUE;
DWORD startime = GetTickCount();
//轉換字符串中的大寫字母爲小寫字母
TransLower(charBuf, fileSize);
DWORD endtime = GetTickCount();
std::cout << "calculate time : " << (endtime - startime) << "ms" << std::endl;
//將字符串寫入另一文件
HANDLE hwfile = CreateFile(L"d:\\lowerchar16M.txt", GENERIC_WRITE, FILE_SHARE_READ, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
if (INVALID_HANDLE_VALUE == hwfile)
{
delete [] charBuf;
charBuf = NULL;
return -1;
}
DWORD nRet;
WriteFile(hwfile, charBuf, fileSize, &nRet, NULL);
CloseHandle(hwfile);
delete [] charBuf;
charBuf = NULL;
int temp;
std::cin >> temp;
return 0;
}
我目前還不知道性能怎麼樣,試了下對16M字符串的轉換,但它已經run5分鐘了還沒run完...待它run完,我們回頭補上這個圖~~各位看官,我又等了20分鐘,它還沒跑完,我們就不跑它了,
下面說我的解決方案(都先用16M的文件作爲測試):
我的第一想法是將大小寫字母統一處理
方案一:使用stl的map數據類型,於是就出現了下面的喪心病狂的key-value對:
void makemaptable(std::map<char, char> &mymap)
{
mymap.insert(make_pair('A', 'a')); mymap.insert(make_pair('B', 'b')); mymap.insert(make_pair('C', 'c')); mymap.insert(make_pair('D', 'd')); mymap.insert(make_pair('E', 'e'));
mymap.insert(make_pair('F', 'f')); mymap.insert(make_pair('G', 'g')); mymap.insert(make_pair('H', 'h')); mymap.insert(make_pair('I', 'i')); mymap.insert(make_pair('J', 'j'));
mymap.insert(make_pair('K', 'k')); mymap.insert(make_pair('L', 'l')); mymap.insert(make_pair('M', 'm')); mymap.insert(make_pair('N', 'n')); mymap.insert(make_pair('O', 'o'));
mymap.insert(make_pair('P', 'p')); mymap.insert(make_pair('Q', 'q')); mymap.insert(make_pair('R', 'r')); mymap.insert(make_pair('S', 's')); mymap.insert(make_pair('T', 't'));
mymap.insert(make_pair('U', 'u')); mymap.insert(make_pair('V', 'v')); mymap.insert(make_pair('W', 'w')); mymap.insert(make_pair('X', 'x')); mymap.insert(make_pair('Y', 'y'));
mymap.insert(make_pair('Z', 'z')); mymap.insert(make_pair('a', 'a')); mymap.insert(make_pair('b', 'b')); mymap.insert(make_pair('c', 'c')); mymap.insert(make_pair('d', 'd'));
mymap.insert(make_pair('e', 'e')); mymap.insert(make_pair('f', 'f')); mymap.insert(make_pair('g', 'g')); mymap.insert(make_pair('h', 'h')); mymap.insert(make_pair('i', 'i'));
mymap.insert(make_pair('j', 'a')); mymap.insert(make_pair('k', 'k')); mymap.insert(make_pair('l', 'l')); mymap.insert(make_pair('m', 'm')); mymap.insert(make_pair('n', 'n'));
mymap.insert(make_pair('u', 'a')); mymap.insert(make_pair('v', 'v')); mymap.insert(make_pair('w', 'w')); mymap.insert(make_pair('x', 'x')); mymap.insert(make_pair('y', 'y'));
mymap.insert(make_pair('z', 'z')); mymap.insert(make_pair('o', 'o')); mymap.insert(make_pair('p', 'p')); mymap.insert(make_pair('q', 'q')); mymap.insert(make_pair('r', 'r'));
mymap.insert(make_pair('t', 't')); mymap.insert(make_pair('s', 's'));
return;
}
爲什麼用map呢,因爲大家都說stl這輪子用起來性能足夠,代碼是:#include<utility>
#include<iostream>
#include<map>
#include<cstdlib>
#include<time.h>
using namespace std;
void makemaptable(std::map<char, char> &mymap)
{
mymap.insert(make_pair('A', 'a')); mymap.insert(make_pair('B', 'b')); mymap.insert(make_pair('C', 'c')); mymap.insert(make_pair('D', 'd')); mymap.insert(make_pair('E', 'e'));
mymap.insert(make_pair('F', 'f')); mymap.insert(make_pair('G', 'g')); mymap.insert(make_pair('H', 'h')); mymap.insert(make_pair('I', 'i')); mymap.insert(make_pair('J', 'j'));
mymap.insert(make_pair('K', 'k')); mymap.insert(make_pair('L', 'l')); mymap.insert(make_pair('M', 'm')); mymap.insert(make_pair('N', 'n')); mymap.insert(make_pair('O', 'o'));
mymap.insert(make_pair('P', 'p')); mymap.insert(make_pair('Q', 'q')); mymap.insert(make_pair('R', 'r')); mymap.insert(make_pair('S', 's')); mymap.insert(make_pair('T', 't'));
mymap.insert(make_pair('U', 'u')); mymap.insert(make_pair('V', 'v')); mymap.insert(make_pair('W', 'w')); mymap.insert(make_pair('X', 'x')); mymap.insert(make_pair('Y', 'y'));
mymap.insert(make_pair('Z', 'z')); mymap.insert(make_pair('a', 'a')); mymap.insert(make_pair('b', 'b')); mymap.insert(make_pair('c', 'c')); mymap.insert(make_pair('d', 'd'));
mymap.insert(make_pair('e', 'e')); mymap.insert(make_pair('f', 'f')); mymap.insert(make_pair('g', 'g')); mymap.insert(make_pair('h', 'h')); mymap.insert(make_pair('i', 'i'));
mymap.insert(make_pair('j', 'a')); mymap.insert(make_pair('k', 'k')); mymap.insert(make_pair('l', 'l')); mymap.insert(make_pair('m', 'm')); mymap.insert(make_pair('n', 'n'));
mymap.insert(make_pair('u', 'a')); mymap.insert(make_pair('v', 'v')); mymap.insert(make_pair('w', 'w')); mymap.insert(make_pair('x', 'x')); mymap.insert(make_pair('y', 'y'));
mymap.insert(make_pair('z', 'z')); mymap.insert(make_pair('o', 'o')); mymap.insert(make_pair('p', 'p')); mymap.insert(make_pair('q', 'q')); mymap.insert(make_pair('r', 'r'));
mymap.insert(make_pair('t', 't')); mymap.insert(make_pair('s', 's'));
return;
}
int main()
{
/*use map as table container*/
std::map<char, char> mymap;
makemaptable(mymap);
/*use array straightly*/
FILE *mystringfile = fopen("F:\\char16M.txt", "r");
FILE *newstringfile = fopen("F:\\data", "w+");
if (mystringfile == NULL || newstringfile == NULL)
{
printf("heheda");
exit(-1);
}
const long LENGTH = 16 * 1024 * 1024;
char *randomstring = new char[LENGTH];/*源隨機字符串
*/
char * upcasestring = new char[LENGTH];/*目標字符串序列
*/
fread(randomstring, sizeof(char), LENGTH, mystringfile);/*讀取源
*/
cout << "src string size:" << strlen(randomstring) << endl;
long long startime = clock();
#pragma omp parallel for/*intel 並行計算庫
*/
for (register long i = 0; i != LENGTH; i++)
{
upcasestring[i] = mymap.at(randomstring[i]);
}
long long endtime = clock();
cout << "run time:" << endtime - startime << "ms" << endl;
fwrite(upcasestring, sizeof(char), LENGTH, newstringfile);
fclose(mystringfile);
fclose(newstringfile);
delete[]randomstring;
delete[]upcasestring;
upcasestring = nullptr;
randomstring = nullptr;
system("pause");
}
這下看看我們的運行效率~~~
16M源字符串:
50多s是吧,考慮到是一個16*1024*1024的數據處理,大概還沒讓人抓狂,那我們實驗一個64M的吧~~~等了好大一會了,我們再等等吧==
結果是:
215秒!果斷受不了,我的電腦也燙的要炸了,畢竟是一個類似死循環的東西~~
開始step2
我們考慮到,對於字母的ascii碼,小寫字母97-122,大寫字母65-90,而我們在運算時,完全可以把字母字符當作對應的整數值,於是,我就做了一個長度爲123的數組 char *alphebaltTable = new char[122];這樣我們就可以直接模擬上面的map<char,char>的效果了,但是速度肯定會提升很多,數組生成代碼如下:
void makearraytable(char *& alphebaltTable)
{
for (int index = 0; index < TABLE_LENGT; index++)
{
if (index >= 65 && index <= 90)
{
alphebaltTable[index] = index + ('a' - 'A');
}
else
alphebaltTable[index] = index;
}
return;
}
總的代碼如下:
#include<utility>
#include<iostream>
#include<map>
#include<cstdlib>
#include<time.h>
using namespace std;
const int TABLE_LENGTH = 122;
void makearraytable(char *& alphebaltTable)
{
for (int index = 0; index < TABLE_LENGTH; index++)
{
if (index >= 65 && index <= 90)
{
alphebaltTable[index] = index + ('a' - 'A');
}
else
alphebaltTable[index] = index;
}
return;
}
int main()
{
/*use array straightly*/
char *alphebaltTable = new char[TABLE_LENGTH];
makearraytable(alphebaltTable);
FILE *mystringfile = fopen("F:\\char16M.txt", "r");
FILE *newstringfile = fopen("F:\\data", "w+");
if (mystringfile == NULL || newstringfile == NULL)
{
printf("heheda");
exit(-1);
}
const long LENGTH = 16 * 1024 * 1024;
char *randomstring = new char[LENGTH];/*源隨機字符串
*/
fread(randomstring, sizeof(char), LENGTH, mystringfile);
cout << "src string size:" << strlen(randomstring) << endl;
char * upcasestring = new char[LENGTH];
long long startime = clock();
#pragma omp parallel for/*intel 並行計算庫
*/
for (register long i = 0; i != LENGTH; i++)
{
*(upcasestring + i) = alphebaltTable[randomstring[i]];
}
long long endtime = clock();
cout << "run time:" << endtime - startime << "ms" << endl;
fwrite(upcasestring, sizeof(char), LENGTH, newstringfile);
fclose(mystringfile);
fclose(newstringfile);
delete[]randomstring;
delete[]alphebaltTable;
alphebaltTable = NULL;
randomstring = nullptr;
delete[] upcasestring;
upcasestring = nullptr;
}
此時,我們看一下運行時間,秒開了!
16M文件:
64M文件:
來個512M文件!:
來個1G的文件:
duang~~讀取錯誤!我們先忽視它,待我開一篇新的博客來討論它;
我們這時看到對於512M的字符串轉換,時間是1460ms,應該來說比較理想了,但我說,這還不夠!
開始step3
原本想通過cuda openmp加速,但發現加了以後,對性能提升基本爲0,偶爾還會下降,主要是因爲我們單次計算量太小了。那怎麼辦?我們人工並行,併合理分配每個任務塊大小!
二話不說,c++11thread走起(如果你還沒用過,納尼太low了,隨便google一發,學習一下吧!):
回到我們原來的問題,無非就是值拷貝,單次任務相關性極低,我們平分這些任務,使其並行化呢,代碼搞起:
#include<time.h>
#include<cstdlib>
#include<iostream>
#include<map>
#include<algorithm>
#include<utility>
#include<thread>
using namespace std;
const int TABLE_LENGT = 122;
void makearraytable(char *& alphebaltTable)
{
for (int index = 0; index < TABLE_LENGT; index++)
{
if (index >= 65 && index <= 90)
{
alphebaltTable[index] = index + ('a' - 'A');
}
else
alphebaltTable[index] = index;
}
return;
}
uint64_t startime;
void mythreadfunc(long &start_series, long & end_series, char * &src_set, char * &alphebaltTable, char * &des_set)
{
long i;
for (i = start_series; i <= end_series; i++)
{
*(des_set+i) = alphebaltTable[src_set[i]];
}
uint64_t diff_time = clock() - startime;
std::cout << "run time:" << diff_time << "ms" << std::endl;
}
int main()
{
char *alphebaltTable = new char[TABLE_LENGT];
makearraytable(alphebaltTable);
FILE *mystringfile = fopen("F:\\char16M.txt", "r");
FILE *newstringfile = fopen("F:\\data", "w+");
if ( mystringfile == NULL || newstringfile == NULL)
{
printf("heheda");
exit (- 1);
}
const long LENGTH = 16 * 1024 * 1024;
char *randomstring = new char[LENGTH];/*源隨機字符串*/
char * upcasestring = new char[LENGTH];
/*應該使用硬件原生cpu核心數,不然的話,很難達到好的性能;
例如,我強制開了8 cpu core,在我的機器上沒有問題,但是,在室友的機器上就會出現很大的數據誤差
原生4core:20ms ////
虛擬8core : 15ms 15ms 15ms 15ms 30ms 30ms 30ms 30ms 後面的虛擬線程顯著拖慢了系統運算時間
*/
const int THREAD_NUM =thread::hardware_concurrency();
long *set_num = new long[THREAD_NUM];/*線程運算元素集合大小
*/
long *start_series = new long[THREAD_NUM];/*開始執行的序列號
*/
long *end_series = new long[THREAD_NUM];/*終止執行的序列號
*/
fread(randomstring, sizeof(char), LENGTH, mystringfile);/*讀取源
*/
{/*操作初始化集合大小
*/
if (LENGTH % (THREAD_NUM) == 0)
{
long average = LENGTH / (THREAD_NUM);
for (int i = 0; i < THREAD_NUM; i++)
{
start_series[i] = average*i;
end_series[i] = average*(i + 1) - 1;
}
}
else
{
int average = LENGTH / (THREAD_NUM - 1);
int mod = LENGTH % (THREAD_NUM - 1);
for (int i = 0; i < THREAD_NUM - 1; i++)
{
set_num[i] = average;
}
set_num[THREAD_NUM - 1] = mod;
for (int i = 0; i < THREAD_NUM; i++)
{
start_series[i] = i*average;
}
for (int i = 0; i < THREAD_NUM; i++)
{
end_series[i] = set_num[i] + start_series[i] - 1;
}
}
}
int thread_num_small = THREAD_NUM - 1;
startime = clock();
for (register int i = 0; i < thread_num_small; i++)
{
//前幾次線程分離,以使操作進行下去
thread t(mythreadfunc, start_series[i], end_series[i], randomstring, alphebaltTable, upcasestring);
t.detach();
}
//最後一次join,試內存得已釋放
thread t(mythreadfunc, start_series[thread_num_small], end_series[thread_num_small]
, randomstring, alphebaltTable, upcasestring);
t.join();
fwrite(upcasestring, sizeof(char), LENGTH, newstringfile);
fclose(mystringfile);
fclose(newstringfile);
delete []randomstring;
delete []alphebaltTable;
alphebaltTable = NULL;
randomstring = nullptr;
delete[] upcasestring;
upcasestring = nullptr;
system("pause");
}
16M字符串測試結果:
64M字符串測試:
512M字符串測試:
512M的字符串轉換 249ms,是一個理想的時間了,但是,我們試試在並行數目上還有沒有文章可做,在前面,我們說過,當線程數超過物理核心(超線程屬於物理核心)時,性能不增反降,但當我們單個任務太繁重時,可不可以適當調節呢?
下面,我給出了使用2倍物理核心線程數的方案測試結果:
額,效率(最慢的thread的時間)還是下降了。。。所以,單個應用還是最大線程數設到物理核心數吧。。。
但是,這個效率還不夠,夜深了,室友都休息了,我明天將會給出windows thread下 及嵌入彙編代碼的優化結果!
晚安!
接着昨天的工作,一直羨慕android開發中的 asynctask,但是今天翻了一下c++11和c++14標準,發現竟然已經支持了原生語言層面的std::async,創建和使用非常簡易,看一下官方的例子就明白了:
#include<future>
#include<iostream>
#include<algorithm>
#include<numeric>
#include<vector>
using namespace std;
template< typename RAIter>
int parrellSum(RAIter beg, RAIter end)
{
RAIter::difference_type len= end-beg;
if (len < 1000)
return std::accumulate(beg, end, 0);
RAIter mid = beg + len / 2;
auto parrellHandler = std::async(std::launch::async, parrellSum<RAIter>, mid, end);
int sum = parrellSum(beg, mid);
return sum + parrellHandler.get();
}
int main()
{
vector<int> v(10000, 1);
int sum = parrellSum(v.cbegin(), v.cend());
cout << sum << "\n";
}
運行結果是10000,看不大懂的同學建議補充一下泛型及模板的知識~哦,還有遞歸。
下面,我們就嘗試模仿上面的例子來優化我們的程序。
#include<time.h>
#include<cstdlib>
#include<iostream>
#include<map>
#include<algorithm>
#include<utility>
#include<thread>
#include<future>
using namespace std;
const int TABLE_LENGT = 122;
void makearraytable(char *& alphebaltTable)
{
for (int index = 0; index < TABLE_LENGT; index++)
{
if (index >= 65 && index <= 90)
{
alphebaltTable[index] = index + ('a' - 'A');
}
else
alphebaltTable[index] = index;
}
return;
}
uint64_t s;
int chunk = 0;
int parrellAsyn(long start_series, long end_series, char * src_set, char * alphebaltTable, char * des_set)
{
long diff_len = end_series - start_series;
if (diff_len < chunk)
{
for (long i = start_series; i != end_series; ++i)
{
*(des_set + i) = alphebaltTable[src_set[i]];
}
return 1;
}
long mid_series = start_series + diff_len / 2;
auto parrellHandler = async(std::launch::async, parrellAsyn, mid_series, end_series, src_set, alphebaltTable, des_set);
int sum = parrellAsyn(start_series, mid_series, src_set, alphebaltTable, des_set);
return sum + parrellHandler.get();
}
int main()
{
char *alphebaltTable = new char[TABLE_LENGT];
makearraytable(alphebaltTable);
FILE *mystringfile = fopen("F:\\char16M.txt", "r");
FILE *newstringfile = fopen("F:\\data", "w+");
if ( mystringfile == NULL || newstringfile == NULL)
{
printf("heheda");
exit (- 1);
}
const long LENGTH = 16 * 1024 * 1024;
char *randomstring = new char[LENGTH];/*源隨機字符串*/
chunk = LENGTH / (4);
char * upcasestring = new char[LENGTH];
fread(randomstring, sizeof(char), LENGTH, mystringfile);
s = clock();
int sum=parrellAsyn(0, LENGTH, randomstring, alphebaltTable, upcasestring);
long e = clock();
cout << sum << endl;
cout << "all runtime :" << (e - s) << endl;
fwrite(upcasestring, sizeof(char), LENGTH, newstringfile);
fclose(mystringfile);
fclose(newstringfile);
delete []randomstring;
delete []alphebaltTable;
alphebaltTable = NULL;
randomstring = nullptr;
delete[] upcasestring;
upcasestring = nullptr;
system("pause");
}
這樣,後臺爲我們開了8個微線程。
可能有朋友會問,既然只開8個線程,爲什麼要用遞歸而不是直接8個異步任務呢,這個問題很好,是時候拿出來官方的解釋了:
Notes
The implementation may extend the behaviorof the first overload of std::async by enabling additional(implementation-defined) bits in the default launch policy.
One drawback with the current definition of std::asyncis that the associated state of an operation launched by std::async can causethe returned std::future'sdestructor to block until the operation is complete. This can limitcomposability and result in code that appears to run in parallel but in realityruns sequentially. For example:
Run this code
{
std::async(std::launch::async, []{ f(); });
std::async(std::launch::async, []{ g(); }); // does not run until f() completes
}
In the above code, f() and g() run sequentiallybecause the destruction of the returned future blocks until each operation hasfinished.
你順序的開這些異步任務並不一定是並行的!
所以我們還是遞歸吧;還是看我們的測試結果:
16M:
32M:
64M:
512M:
可以看出,速度有了不少提升!而且async的易用性也很強!
由於時間原因和vs中潛入彙編存在很多問題,所以優化之路就暫時到這兒了。