16M/512M混合字符串大小寫轉換最快有多快!-也談大數據下的性能優化深入探究

    最近,計算機系統老師佈置了道作業: 給定一個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中潛入彙編存在很多問題,所以優化之路就暫時到這兒了。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章