반응형

 

#include "pch.h"
#include <iostream>
#include "CorePch.h"
#include <thread>	//11부터 thread 가 다른 플랫폼에서도 구현 가능되도롯 멀티플랫폼으로 지원 됨 => linux 에서도 실행 가능
#include <vector>

using namespace std;

void HelloThread(int a)
{
	std::cout << "Hello Thread " << a << std::endl;
}


int main()
{
	vector<std::thread> t;
	t.resize(1);

	auto idtest = t[0].get_id();			//관리하고 있는 스레드가 없다면 id 는 0

	t[0] = std::thread(HelloThread, 10);

	if (t[0].joinable())
	{
		t[0].join();
	}

	std::cout <<" main " << std::endl;

	


	return 0;
}

 

 

반응형
반응형

#include "pch.h"
#include <iostream>
#include "CorePch.h"
#include <thread> //11부터 thread 가 다른 플랫폼에서도 구현 가능되도롯 멀티플랫폼으로 지원 됨 => linux 에서도 실행 가능

void HelloThread()
{
std::cout << "Hello Thread" << std::endl;
}


int main()
{
//cout 은 os 커널에 요청하여 요청된 처리가 다시 콘솔화면으로 오는 느린 실행이다
//HelloWorld();

//std::thread t(HelloThread);
std::thread t;

auto idtest = t.get_id(); //관리하고 있는 스레드가 없다면 id 는 0

t = std::thread(HelloThread);


int32 count = t.hardware_concurrency(); //CPU 코어 개수는 몇개인지 힌트를 줌 => 논리적으로 실행할 수 있는 프로세스 개수 ,  100% 확실한 동작이 되진 않을 수있어서 경우에따라 0 을리턴하면 제대로 조사가 안된 것  

auto id = t.get_id(); // 각 쓰레드마다 부여되는 id , 쓰ㅜ레드 사이에서는 id 가 겹치지 않는다
t.detach(); //join 반대로 std::thread 객체에서 실제 스레드를 분리 => 스레드 객체 t 와 연결을 끊어줌 => 만들어진 스레듸이 정보를 더이상 사용 할수 없음

//쓰레드를 떼어내면, 쓰레드가 독립적으로 실행됩니다. 프로그램이 종료되면 떼어진 쓰레드는 동작이 멈추게 됩니다. 


bool states = t.joinable(); //detach 되거나 연동된 슬레드가 없는 상태를 판별하기 위한 함수

//t.join(); //스레드가 끝날때까지 대기

if (t.joinable())
{
t.join();
}


std::cout << "Hello Main" << std::endl;

return 0;
}

 

 

위 코드에선 detach 하여 Join 을 하지 못하기 때문에 Hello Main 이 높은 확률로 나오는 것을 알 수 있다

반응형
반응형

RxJava - 물리적인 쓰레드와 논리적인 쓰레드의 이해

 

물리적인 쓰레드와 논리적인 쓰레드의 이해

  • 물리적인 쓰레드는 하드웨어와 관련이 있고, 논리적인 쓰레드는 소프트웨어와 관련이 있다.
  • 물리적인 쓰레드를 이해하기 위해서는 CPU의 코어를 먼저 알아야 한다.
  • 그럼 코어란?
    • CPU의 명령어를 처리하는 반도체 유닛
    • 코어의 갯수가 많으면 명령어를 병렬로(parallel) 더 많이 더 빠르게 처리할 수 있다.
  • 물리적인 쓰레드는 물리적인 코어를 논리적으로 쪼갠 논리적 코어이다.

논리적인 쓰레드란?

  • 자바 프로그래밍에서 사용하는 그 쓰레드가 논리적인 쓰레드이다.
  • 논리적인 쓰레드는 프로세스 내에서 실행되는 세부 작업의 단위이다.
  • 프로세스는 컴퓨터에서 실행할 수 있는 실행 파일(프로그램)을 실행하면 생기는 인스턴스이다.
  • 논리적인 쓰레드의 생성 개수는 이론적으로는 제한이 없지만 실제로는 물리적인 쓰레드의 가용 범위내에서 생성할 수 있다.

물리적인 쓰레드와 논리적인 쓰레드의 이해

  • 병렬성 : 실제로 작업들이 병렬로 작업되는 성질 (동일한 시간대에 여러개의 쓰레드들의 동시에 실행이된다.)
  • 동시성 : 실제로 병렬적으로 실행이 되는 것처럼 보이지만 여러 개의 작업이 짧은 시간에 번갈아 가면서 병렬로 처리되는 것처럼 시행되는 성질을 동시성이라고 한다.

 

ref : https://yunzai.dev/posts/RxJava_%EB%AC%BC%EB%A6%AC%EC%A0%81%EC%9D%B8_%EC%93%B0%EB%A0%88%EB%93%9C%EC%99%80_%EB%85%BC%EB%A6%AC%EC%A0%81%EC%9D%B8_%EC%93%B0%EB%A0%88%EB%93%9C%EC%9D%98_%EC%9D%B4%ED%95%B4/

반응형
반응형


Thread란

하나의 프로그램내에서 여러 개의 실행 흐름을 두기 위한 모델

하나의 프로세서(실행 중인 프로그램)에서 각 독립적인 일의 단위인 스레드(Thread)로 여러 작업을 처리할 수 있다. 즉 하나의 프로세서에서 병렬적으로 여러 개 작업을 처리하기 위해서는 각 작업을 스레드화하여 멀티스레딩이 가능하게 해야 한다.

Process란

간단하게 말하면 실행중인 프로그램.

프로세스는 사용 중인 파일, 데이터, 프로세서의 상태, 메모리 영역 주소 공간, 스레드 정보, 전역 데이터가 저장된 메모리 부분 등 수 많은 자원을 포함하는 개념. 종종 스케쥴링의 대상이 되는 작업이라고 불리기도 함

쓰레드와 프로세스의 차이점

프로세스는 완벽히 독립적이기 때문에 메모리 영역(Code, Data, Heap, Stack)을 다른 프로세스와 공유를 하지 않지만, 쓰레드는 해당 쓰레드를 위한 스택을 생성할 뿐 그 이외의 Code, Data, Heap영역을 공유한다.

텍스트 : PC(피시 카운터 - 다음번에 실행 될 명령어의 주소를 가지고 있는 레지스터), 프로그램 코드 저장

데이터: 글로벌 변수, 스태틱 변수 저장

 :메모리관리, 동적 메모리 할당(시스템 콜로 관리)

스택: 임시 데이터 저장- 로컬 변수, 리턴 어드레스

스택을 독립적으로 할당하는 이유

  • 스택은 함수 호출 시 전달되는 인자, 되돌아갈 주소값 및 함수 내에서 선언하는 변수 등을 저장하기 위해 사용되는 메모리 공간이다. 따라서 스택 메모리 공간이 독립적이라는 것은 독립적인 함수 호출이 가능하다는 것이고, 이는 독립적인 실행 흐름이 추가되는 것이다. 결과적으로 실행 흐름의 추가를 위한 최소 조건이 독립된 스택을 제공하는 것이다. 

코드 영역을 공유

  • 프로세스는 독립적인 구조이기 때문에 다른 프로세스의 Code영역에 있는 함수를 호출할 수 없다.
  • 쓰레드는 Code영역을 공유하기 때문에 두 개 이상의 쓰레드가 자신이 포함된 프로세스의 Code영역에 있는 함수를 호출할 수 있다.

데이터 영역과 힙 영역을 공유

  • 전역 변수와 동적 할당된 메모리 공간을 공유할 수 있고, 이를 통해 쓰레드 간 통신을 할 수 있지만  동시에 메모리에 접근하기 때문에 주의해야 한다.

멀티 프로세스의 문제점

  • 두 개의 프로세스는 완전히 독립된 두 개의 프로그램 실행을 위해서 사용되기 때문에 컨텍스트 스위칭(프로세스의 상태 정보를 저장하고 복원하는 일련의 과정)으로 인한 성능 저하가 발생
  • 쓰레드는 하나의 프로그램 내에서 여러 개의 실행 흐름을 두기 위한 모델이다.
  • 쓰레드는 프로세스처럼 완벽히 독립적인 구조가 아니다. 쓰레드들 사이에는 공유하는 요소가 있다.
  • 쓰레드는 이 공유하는 요소로 인해 컨텍스트 스위칭에 걸리는 시간이 프로세스보다 짧다.

쓰레드가 프로세스보다 컨텍스트 스위칭이 빠른 이유

 쓰레드가 프로세스보다 컨텍스트 스위칭이 빠른 이유는 메모리 영역을 공유하기 때문이다. 실제로 공유되는 데이터가 있고 아닌 데이터가 있다.

sp(Stack Pointer), fp(Frame Pointer), pc(Program Counter) 레지스터

  • pc는 실행해야 할 명령어의 위치를 가리키는 레지스터이다.
  • 쓰레드별로 main함수를 독립적으로 가지고 있고, 함수 호출도 독립적으로 진행되기 때문에 쓰레드별로 현재 pc값은 달라야 한다. 따라서 pc는 공유되지 않는다.
  • 쓰레드는 독립적인 스택을 가지기 때문에 스택의 정보를 담고 있는 sp와 fp는 공유되지 않는다.

범용적으로 사용가능한 레지스터

  • 시스템을 어떻게 디자인 하느냐에 따라 달라지는 것이기 때문에 일반적으로 공유된다고 할 수 없다.

캐시메모리

  • 캐시 메모리는 CPU에서 한번 이상 읽어 들인 메인 메모리의 데이터를 저장하고 있다가 CPU가 다시 그 메모리에 저장된 데이터를 요구할 때 메인 메모리를 통하지 않고 바로 값을 전달하는 용도로 사용된다.
  • 프로세스 사이에서 공유하는 메모리가 하나도 없기 때문에 컨텍스트 스위칭이 발생하면 캐쉬에 있는 모든 데이터를 모두 리셋하고 다시 캐쉬 정보를 불러와야 한다.
  • 쓰레드는 캐쉬 정보를 비울 필요가 없기 때문에 프로세스와 쓰레드의 컨텍스트 스위칭 속도의 차이는 이때 발생한다.

Windows에서의 프로세스와 쓰레드

  • Windows 입장에서 프로세스는 쓰레드를 담는 상자에 지나지 않는다.
  • 또한 Windows에서 프로세스는 상태(Running, Ready, Blocked)을 지니지 않는다. 상태를 지니는 것은 스레드이다.
  • 스케줄러가 실행의 단위로 선택하는 것도 프로세스가 아닌 스레드이다.
  • 프로세스 컨텍스트 스위칭 역시 실제로 오버헤드가 발생하는 부분은 서로 다른 프로세스에 포함된 스레드 간의 컨텍스트 스위칭시 발생한다.







[OS]메모리 관점에서 본 쓰레드(thread)



메모리 관점에서 본 process의 특징

 각각의 프로세스는 메모리 공간에서 독립적으로 존재한다.



<출처:https://elgaabeb.wordpress.com/>


 전에 본 적이 있는 그림이다. 이 그림은 프로세스를 구성하는 메모리 공간의 모습이다. 각각의 프로세스는 자신만의 이런 메모리 구조를 가진다. 프로세스A, B, C가 존재한다면 각각 프로세스는 모두 위와 같은 구조의 메모리 공간을 가진다.

 독립적인만큼 다른 프로세스의 메모리 공간에 접근할 수도 없다. A가 B의 메모리 공간에 접근하면 재앙이 발생할 수도 있다. 만약 A가 뜬끔없이 Chrome의 메모리 공간에 접근한다고 생각해보라. Chrome의 안정성이 보장될 수 있겠는가? 더 뜬금없이 A가 windows의 메모리 공간에 접근한다고 생각해보라. 운영체제의 메모리 공간에 접근하여 뭔가를 변경한다면 심각한 문제가 발생할 수 있다.(물론 운영체제의 메모리 공간에 접근하는 것은 원천적으로 불가능하다) 그러므로 프로세스의 안정성을 보장하기 위해서는 프로세스는 각각 독립된 메모리 공간을 가져야 한다.

 그러면 혹시 A에서 연산한 결과를 B에서 받아서 사용하고 싶다면 어떻게 해야할까?


IPC(inter process communication)




<출처: http://madhusudhanrc.blogspot.kr/2012/08/inter-process-communicationipc.html>


 A의 메모리 공간에 B가 직접 접근하지 못하기 때문에 프로세스간의 통신을 하는 특별한 방법들이 존재한다. 메일슬롯(mailslot), 파이프(pipe) 등이 바로 프로세스 간의 통신 즉, IPC의 예들이다.

 IPC에 대한 자세한 설명은 생략~

 중요한 점은 프로세스는 독립적인 메모리 공간을 지니기 때문에 IPC를 통하지 않고는 통신할 수 없다는 사실이다. 그리고 프로세스가 여럿이 병렬적으로 실행되기 위해서는 필연적으로 컨텍스트 스위칭이 발생할 수 밖에 없다.


프로세스가 지니는 제한

 독립적인 메모리 공간으로 컨텍스트 스위칭이 발생한다.
 통신하기 위해서는 IPC가 필요하다.

 이 두 가지 문제점을 한 번에 해결할 수 있는 녀석이 쓰레드이다.


쓰레드

  쓰레드(thread)는 하나의 프로그램 내에 존재하는 여러 개의 실행 흐름을 위한 모델이다. 우리가 생각하는 프로그램이 실행되기 위해서 하나의 실행흐름으로 처리할 수도 있지만 다수의 실행흐롬으로 처리할 수도 있다.(multi-thread)

 다시 말해서 프로세스에 존재하는, 프로세스가 실행되는 흐름이다.






<출처: http://en.wikipedia.org/wiki/Thread_(computing)>


 wikipedia를 보면 쓰레드를 설명하기 위해 위와 같은 그림들을 보여준다. 아래 그림을 보면 프로세스 내부에 2개의 쓰레드가 존재한다. 그리고 시간의 방향을 따라 쓰레드가 실행되고 있다. 그림에서 볼 수 있듯이 쓰레드는 프로세스와 별개가 아닌 프로세스를 구성하고 실행하는 흐름이다.

 프로세스에서도 그러했듯이 이번에도 메모리 관점에서 쓰레드를 보자. 프로세스와 어떤 차이가 있는지 뚜렷하게 알 수 있을 것이다.


메모리 공간에서의 쓰레드

<출처: http://www-01.ibm.com/support/knowledgecenter/SSLTBW_1.12.0/com.ibm.zos.r12.euvmo00/euva3a00451.htm>

 위 그림은 프로세스와 쓰레드의 메모리 구조의 차이점을 보여준다. 왼쪽의 프로세스는 이미 봤기 때문에 설명하지 않고, 오른쪽의 thread에 주목하자. 앞서 말한 것처럼 쓰레드는 프로세스 안에 존재하는 실행흐름이다. 메모리 구조 역시 그러하다. 하지만 특이한 점은 쓰레드는 프로세스의 heap, static, code 영역 등을 공유한다는 사실이다. 각각의 프로세스가 독립적인 stack, heap, code, data 영역을 가진 반면에, 한 프로세스에 속한 쓰레드는 stack 영역을 제외한 메모리 영역은 공유한다. 

//첨언 : 쓰레드는 stack 뿐만 아니라 레지스터또한 독립적인 공간이다
//굳이 중요도를 따지자면 레지스터의 독립적인 공간에 좀 더 관심을 둘 필요가 있다

 쓰레드가 code 영역을 하기 때문에 한 프로세스 내부의 쓰레드들은 프로세스 가 가지고 있는 함수를 자연스럽게 모두 호출할 수 있다.

 뿐만 아니라 쓰레드는 data, heap 영역을 공유하기 때문에 IPC 없이도 쓰레드 간의 통신이 가능하다. 동일한 프로세스 내부에 존재하는 쓰레드 A, B가 통신하기 위해 heap 영역에 메모리 공간을 할당하고, 두 쓰레드가 자유롭게 접근한다고 생각하면 된다.

 쓰레드는 프로세스처럼 스케줄링의 대상이다. 이 과정에서 컨텍스트 스위칭이 발생한다. 하지만 쓰레드는 공유하고 있는 메모리 영역 덕분에 컨텍스트 스위칭 때문에 발생하는 오버헤드(overhead)가 프로세스에 비해 작다. 



ref : http://brownbears.tistory.com/39

ref : https://mooneegee.blogspot.com/2015/01/os-thread.html


반응형
반응형

모바일 AP에서 쓰로틀링(Throttling)이란?



모바일 AP 쪽에 관심이 있으신 분들은 ‘쓰로틀링(Throttling)’이라는 표현을 보셨을 겁니다. ‘목을 조르다’ 는 뜻의 영어 단어 ‘throttle’에서 온 단어로 모바일 AP 쪽에서는 목을 조르듯이 AP의 클럭을 꽉 조여서 발열을 억제하는 것을 말합니다.

 

PC의 CPU나 GPU, 모바일 AP 같은 반도체들은 일을 하면서 많든 적든 열을 발생시킵니다. PC의 CPU 같은 경우는 주로 팬을 쓰는 공랭식으로 열을 해결해서 발열이 일정 이상 넘어가면 PC에서 선풍기 돌아가는 굉음이 나게 됩니다.

 

그런데, 모바일 AP의 경우는 들어가는 곳이 스마트폰이나 태블릿 등의 협소한 공간이다 보니 발열을 잡기 위한 팬이나 기타 냉각 장치를 사용하기가 어렵습니다. 그렇다고 발열을 잡지 않을 수도 없는 만큼, 온도가 일정한 수준을 넘으면 AP의 작동 클럭을 강재로 낮춰서 발열을 억제합니다.

 

AP의 동작 클럭은 높으면 발열이 심해지지만 성능은 좋아집니다. 마찬가지로 클럭이 낮아지면 발열은 적어지지만 대신 성능도 함께 떨어지게 됩니다. 그러니 쓰로틀링으로 발열을 잡는다는 말은 결국 성능을 희생해서 급한 불을 끈다는 개념이지요. 모바일 AP에서 쓰로틀링이 심하다는 표현은 갑작스러운 성능 하락이 심하다는 말과 동의어입니다.

 

그러면, 이런 쓰로틀링이 심한 AP는 어떤 것이 있을까요? 아니면 쓰로틀링이 적은 AP는요? mobiledroid라는 사이트에서 현재 주로 쓰이는 상위 AP들을 상대로 쓰로틀링이 얼마나 심한지 측정해봤습니다.

 

일단 테스트에 참가한 모바일 AP들과 탑재하고 테스트에 임한 기기들의 명단은 아래 표와 같습니다.

 

탑재 기기제조사AP
iPhone 6애플A8
갤럭시 노트 4삼성엑시노스 5433
갤럭시 S6삼성엑시노스 7420
갤럭시 노트 4퀄컴스냅드래곤 805
LG G4퀄컴스냅드래곤 808
HTC One M9퀄컴스냅드래곤 810
ASUS Zenfone 2인텔아톰 Z3580

 

CPU 쓰로틀링 테스트




 

CPU 쓰로틀링 테스트는 GeekBench 결과를 기준으로 최고값에 대한 최저값의 비율을 구한 것입니다. 예를 들어 –40%라는 값은 최고값 100에 대해 최저값은 40이 낮다는 말입니다.

 

결과를 보면 애플 A8 프로세서와 퀄컴의 스냅드래곤 810 프로세서가 유난히 눈에 띕니다. 애플 A8의 경우 쓰로틀링 정도가 –10%를 조금 낮은 수준으로 거의 없다시피 하는데 반하여, 퀄컴 스냅드래곤 810의 경우 쓰로틀링 정도가 –60%를 넘는 것을 알 수 있습니다.

 


 

이것은 GeekBench 최고값과 평균값을 비교한 것으로 퀄컴 스냅드래곤 810의 심한 쓰로틀링이 더욱 두드러지게 나타납니다.

 

GPU 쓰로틀링 테스트

 

 

GPU 쓰로틀링 테스트는 GFXBench T-Rex Off-Screen 결과를 기준으로 CPU 때와 마찬가지로 최고값에 대한 최저값의 비율을 비교했습니다.

 

이번 결과에서도 애플 A8은 쓰로틀링이 거의 없는 모습을 보이며 압도적으로 우수한 면모를 나타냈습니다. 그리고 퀄컴의 스냅드래곤 808이 그 뒤를 이어 좋은 모습을 보여주는 군요.

 

CPU와 달리 GPU 쓰로틀링 테스트에서는 퀄컴 스냅드래곤 810이 평균적인 성적을 거뒀습니다. 좀 더 정확하게 말하면 우수한 성적을 남긴 애플 A8과 퀄컴 스냅드래곤 808, 그리고 인텔 아톰 Z3580을 제외한 나머지 AP들은 모두 비슷한 결과를 보여줍니다.

 

총평

사실 이 테스트에는 상당한 문제가 있습니다. 같은 AP라도 기기 제조사에 따라 쓰로틀링을 얼마나 걸지가 다릅니다. 어떤 제조사는 온도가 50도만 되도 클럭다운을 시작하고 어떤 제조사는 70도까지는 쓰로틀링을 걸지 않습니다. 그리고 같은 제조사라도 제품에 따라 쓰로틀링이 다릅니다. 비교적 여유 공간이 있는 패블릿은 상대적으로 발열에 강해서 다른 방법으로 AP를 냉각하고 스마트폰 같은 경우는 쓰로틀링 외에는 열에 대한 대책이 전혀 없으므로 클럭다운에 의존하는 바가 큽니다. 그러니 위의 테스트가 정확하게 AP의 쓰로틀링을 반영했다고 할 수는 없습니다.

 

그런 점을 생각하면서 결과를 보면 테스트는 사실 이미 전부터 알려져 있던 사실의 확인에 가깝습니다. 이번 테스트에서 최고의 성적을 기록한 애플 A8이 포함된 애플의 A 시리즈 칩셋들은 본래 쓰로틀링이 적기로 정평이 나있었습니다. 극단적일 정도로 애플의 iOS 기기에 최적화를 하고 비교적 낮은 클럭에서 작동해서 발열의 여지를 많이 줄였지요.

 

마찬가지로, 최악의 모습을 보여준 퀄컴의 스냅드래곤 810의 경우 이미 AP 제조사 퀄컴과 810을 사용하는 기기 제조사들을 제외하면 심각한 발열 문제 때문에 쓰로틀링이 심하기로 평판이 자자합니다.

 

이번 쓰로틀링 테스트는 새로운 사실을 알아 내었다기 보다는 기존에 알고 있었던 사실을 확인한 점에 의의를 두어야 할 것 같습니다.




ref : https://m.blog.naver.com/PostView.nhn?blogId=viperf40&logNo=220403927497&proxyReferer=https%3A%2F%2Fwww.google.co.kr%2F



반응형
반응형

SIMD 연산은 C언어에서 += 연산과 유사한 동작을 한다


예)


int a,b;


a += b;


와 유사한 연산을 한다





B : byte

W :word            

D : double word  = 4 byte

Q : quad word    = 8 byte


S는 signed US 는 unsiged 를 나타냄으로


PADDUSW 의 경우는?


위 규칙대로 해석한다면 Unsigned Word 타입으로 unsiged short : 16 bit 이며 오버플로우시 + 부호를 유지한다로

볼 수 있다








배열에 값 담아 놓고 c++ 로 더할때와 simd 를 사용해서 더할때와의 속도차 비교

[소스코드 날림 주의.. ㅋ]


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
#include <iostream>
#include <fstream>
#include <windows.h>
#include <string.h>
#include <chrono>
 
 
 
int totalSum(int sizeint data[])
{
    int sum=0;
    for(int i=0;i< size++i)
    {
        sum += data[i];
    }
    
    return sum;
}
 
 
int main()
{
 
 
    const int unit = 16;
    const int size = 40002;
    int _package=4, totalByte =0;
 
    int data[size= { 0, };
    int result[4= {0,};
 
    int quotientByte = 0;
    int quotient = (size * _package) / unit;                    // 16 byte로 떨어지는 개수, 소수점 버림
    int remainder = (size * _package) % unit;                // 16 byte 이외의 엽ㄴ
    int total = 0;
 
 
    std::fill(data, &data[size], 2);
 
 
    std::chrono::system_clock::time_point start1 = std::chrono::system_clock::now();
    std::cout<<"C++ total : "<<totalSum(size,data)<<std::endl;        //80004
    std::chrono::duration<double> sec1 = std::chrono::system_clock::now() - start1;
 
    std::cout << "simd 걸린 시간(초) : " << sec1.count() << " seconds" << std::endl;
 
 
 
 
    
    std::chrono::system_clock::time_point start = std::chrono::system_clock::now();
 
    __asm {
 
        pushad
        
        mov eax, 16
        mov ecx, quotient
        mul ecx
        mov quotientByte, eax
 
        lea esi, data
        
        pxor xmm1, xmm1
        mov ecx, 0
 
        loopAdd:
        movdqu xmm0, [esi + ecx]            //16 byte 를 xmm0 에 복사
        paddd xmm1, xmm0
 
        add ecx, unit
        cmp ecx, quotientByte
        jl    loopAdd                                //0이 아니면
 
        movdqu result, xmm1
 
        popad
        emms
    }
 
 
    //일단 지금까지의 합 구하기 (이 구문은 나중에 simd 로 한번에 처리할 수 있음)
    
    for (int i=0;i<_package;++i)
    {
        total += result[i];
    }
 
    //나머지 여분 값 더하기
    for(int i= quotientByte/_package; i< (quotientByte +remainder)/ _package; ++i)
    {
        total += data[i];
    }
 
    std::chrono::duration<double> sec = std::chrono::system_clock::now() - start;
 
    std::cout << "simd 걸린 시간(초) : " << sec.count() << " seconds" << std::endl;
 
 
    std::cout << "SIMD total : " << total << std::endl;        //80004
 
    system("pause");
 
 
    return 0;
}
 
 
 
 
 

cs






simd 연산으로 더한 결과가 훨씬 빠름을 알 수 있다

최종 total 값이 자료형의 범위를 넘지 않는다면 더 작은 바이트(4바이트가 아닌 2바이트)에서 연산 할 경우 더 높은 성능향상을

기대할 수 있다





반응형
반응형


SIMD 의 종류와 MMX

 

여기서는 Intel x86 아키텍쳐의 SIMD 만 설명하겠습니다.

먼저 Pentium 시리즈와 함께 처음 나온 MMX 가 있습니다.

이 집합은 FPU(x87) 에서 사용되는 레지스터를 공유해서 사용합니다.

즉 따로 MMX 용 레지스터가 존재하지는 않습니다.

 

그리고 MMX 후에 나온 SSE 가 있습니다.

아마 모두 한 번쯤은 들어보셨을 정도로 대표적인 SIMD 입니다.

(이름 자체도 Streaming SIMD Extensions 의 약자입니다.)

xmm 이라는 128비트 레지스터 8개가 추가되어 SIMD 전용 레지스터로 사용됩니다.

http://cafe.naver.com/cafec/243004



EMMS 를 사용하는 이유는 


The EMMS Instruction: Why You Need It

Using EMMS is like emptying a container to accommodate new content. The EMMS instruction clears the MMX™ registers and sets the value of the floating-point tag word to empty.

You should clear the MMX™ registers before issuing a floating-point instruction because floating-point convention specifies that the floating-point stack be cleared after use. Insert the EMMS instruction at the end of all MMX™ code segments to avoid a floating-point overflow exception.

Why You Need EMMS to Reset After an MMX™ Instruction





CAUTION

Failure to empty the multimedia state after using an MMX™ technology instruction and before using a floating-point instruction can result in unexpected execution or poor performance.

https://software.intel.com/en-us/node/524274




반응형
반응형





movdqu

mov : asm 에서의 mov 와 유사한 의미
dq  : double quad (word) 로 quad word는 word 의 4배 인 8 byte 와 
      앞의 double 이 붙어 2*8 = 총 16 바이트인데 word 가 기본 pack 사이즈를 말함으로 2바이트씩 나눈 배열을 말함
u   : unalign 으로 메모리에 있는 값들이 정렬되지 않은 값을 레지스터로 옮길때 붙이는 첨자
      (정렬되지 않은것은 캐쉬미스가 발생하여 오래 걸릴수 있음, 하단 그림 참조)



movdqa

a : align  : 메모리가 정렬된 상태로 정렬돈 메모리를 레지스터로 복사 시킬때에는 16개의 바이트의 메모리가 모두
             붙어 있기 때문에 캐쉬에서 레지스터로 옮길때 부하없이 로드할 수 있다 (하단 그림 참조)
a를 제외한 나머지는 movdqu 에서 movdq 들과 동일한 의미









SIMD 로 배열 copy 작성

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
int main(int argc, char* argv[])
 
{
 
    short A[8= {1,2,3,4,5,6,7,8};
 
    short B[8= {0};        
 
    __asm
 
    {
 
        pushad
 
        movdqu  xmm0, A    //한 사이클에 모두 복사된다    
 
        movdqu  B, xmm0    //한 사이클에 모두 복사된다    
 
        popad
 
        emms
 
    }
 
    printf("B : %d,%d,%d,%d,%d,%d,%d,%d\n",B[7],B[6],B[5],B[4],  B[3],B[2],B[1],B[0]);
 
    return 0;
 
}

cs



배열에서 xmm0 레지스터로 copy 되는 내역을 볼 수 있는데 순서는 반대로 copy 된다
xmm0 에서 배열로 copy 될때도 반대로
그리하여 다시 배열로 올때는 일반적으로 예상할 수 있는 A 상태 그대로의 순서대로 오는 것을 알 수 있다


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
-        xmm0    {m128_f32= {1.837e-40#DEN, 3.673e-40#DEN, 5.510e-40#DEN, 7.347e-40#DEN}  m128_f64= {5.562748306789e-309#DEN, ...} ...}    __m128
+        m128_f32    {1.837e-40#DEN, 3.673e-40#DEN, 5.510e-40#DEN, 7.347e-40#DEN}    float[4]
+        m128_f64    {5.562748306789e-309#DEN, 1.112551783418e-308#DEN}    double[2]
+        m128_i8    "\x1"    char[16]
-        m128_i16    {12345678}    short[8]
        [0]    1    short
        [1]    2    short
        [2]    3    short
        [3]    4    short
        [4]    5    short
        [5]    6    short
        [6]    7    short
        [7]    8    short
+        m128_i32    {131073262147393221524295}    int[4]
+        m128_i64    {11259127918755852251829878849541}    __int64[2]
+        m128_u8    "\x1"    unsigned char[16]
+        m128_u16    {12345678}    unsigned short[8]
+        m128_u32    {131073262147393221524295}    unsigned int[4]
+        m128_u64    {11259127918755852251829878849541}    unsigned __int64[2]
        eax    39226256    unsigned int

cs



XMM0 = 0008000700060005-0004000300020001 

XMM1 = 0000000000000000-0000000000000000 

XMM2 = 0000000000000000-0000000000000000 

XMM3 = 0000000000000000-0000000000000000 

XMM4 = 0000000000000000-0000000000000000 

XMM5 = 0000000000000000-0000000000000000 

XMM6 = 0000000000000000-0000000000000000 

XMM7 = 0000000000000000-0000000000000000 





결과

B : 8,7,6,5,4,3,2,1





[movdqa]
정렬된 데이이터의 데이터 복사

__declspec(align(16)) 과 movdqa를 사용한 예이며 결과는 movdqu 와 동일하다


단 주의해야하는 점은 movdqa 의 경우 정렬된 데이터를 취급함으로 __declspec(align(16)) 를 배열에서 빼고
실행할 경우 크래쉬가 발생한다

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int main(int argc, char* argv[])
{
 
    __declspec(align(16)) short A[8= {1,2,3,4,5,6,7,8};
    __declspec(align(16)) short B[8= {0};
    __asm
    {
        pushad
        movdqa  xmm0, A        
        movdqa  B, xmm0        
        popad
        emms
    }
    printf("B : %d,%d,%d,%d,%d,%d,%d,%d\n",B[7],B[6],B[5],B[4],  B[3],B[2],B[1],B[0]);
    return 0;
}
 

cs



반응형
반응형

simd 는 범용레지스터가 아닌 전용 레지스를 사용한다


32bit : xmm0 ~ xmm7

64bit : xmm0 ~ xmm15



와 같은 레지스터들을 갖고 simd 명령어는 다음 처럼 사용 한다


simd 명령어  p + operation + type






예를 들어


paddsw 라고 한다면


p는 packed 를 의미하는 첨자이고 

add 는 일반 어셈블리의 add 와 유사한 내용이며 

sw 가 의미하는 것은 각각 s : signed,   w : word  라는 접미사의 내용이다


를 의미 하는 것으로 


2바이트로 팩된 데이터들을 한 사이클에 8개의 add 연산을 하게하는 명령어를 말한다



paddusw 는  unsigned word 를 뜻한다







반응형
반응형



위 그림에서 Register Bank 에 대해 좀더 자세히 알아본다면


레지스터 뱅크는 레지스터들이 모여있는곳을 말한다



좀더 자세히 보면




범용 레지스터(General Register) 와 XMM 전용 레지스터(XMM0~XMM7) 들이 있다

* 32bit 컴퓨터는 32bit 레지스터를 사용 64bit 컴퓨터는 레지스터 또한 64 bit 이다


범용레지스터 네이밍 (Extention) E(A~D)X,  ESI(ESourceIndex), EDI(EDestinationIndex),  ESP, EBP 이 두게는 스택 포인터 베이스 포인터의 네이밍 

(의미를 알면 외우기가 좀더 편함)


64bit 로 가면 앞에 접두어가 E 였던것이 R 로 바뀌게 된다

R(A~D)X 의 형태임이고 64 bit 에서는 32bit와는 다르게 8개의 레지스터가 위 그림처럼 더 추가 된다(R8~R15)


그리고 RAX 안에는 EAX 가 포함된 형태가 된다 위 그림에서 하얀색 부분을 EAX 라 보면 됨


XMM 레지스터의 경우에는 64bit 로 가면서 8 개가 위 그림처럼 더 추가된다










반응형
반응형



C에서 선언한 변수들은 스택, 동적으로 할당된것은 heap 에 있게 되고 그것들이 메모리영역에 올라와 있게 되고


만약 

int a=3,b=7, c;

c = a + b;


의 연산을 한다고 할 경우


먼저 데이터들 a, b 를 캐쉬 그리고 캐쉬에서 General Register 들로 옮겨야 한데 이때 옮기는 역할을 하는 것이

LSU 가 하게 된다 (올리기도 내리기도 한다)


이때 올라간 데이터가 정수라면 ALU 를 통해서 만약 올라간 데이터가 float 이면 FPU 를 통해서 연산되고


현재는 정수임으로 ALU 에서 연산된다고 할 경우 + 에 해당하는 명령어의 경우는 이러한 연산 명령어들이 모여 있는 곳인



명령어 세트에서 가져와 L1 Instruction cache(L1 명령어 캐쉬)  로 로드 된다음 이 명령어를 ALU 로 이동시킨다음 두 변수와 연산한 


결과를 다시 레지스터에 저장한다음 이 값을 다시 LSU 를 통해서 메모리영역 변수 C 에 최정 저장하게 되는 과정을 거친다










반응형
반응형



인텔® 스트리밍 SIMD 확장 기술


마지막 검토일: 25-Jul-2017
문서 ID: 000005779

인텔® 스트리밍 SIMD 확장은 무엇입니까?

  • single instruction multiple data(SIMD)
  • 인텔® 스트리밍 SIMD 확장(인텔® SSE)

스트리밍 SIMD 확장(sse)

SSE는 single instruction multiple data을 가능하게 하는 프로세서 기술. 이전 프로세서만 프로세스 당 명령을 단일 데이터 요소. SSE 명령을 여러 데이터 요소를 처리하는 수 있습니다. 3d 그래픽, 빠른 처리와 같은 많은 응용 프로그램에서 사용됩니다.

SSE는 MMX™ 기술 교체하도록 설계되었습니다. 이 확장된 세대의 인텔® 프로세서를 포함 SSE2, SSE3/SSE3S, 및 SSE4. 각 반복가 새로운 명령과 향상된 성능 제공.

Streaming SIMD Extensions 2(SSE2)

SSE2 는 다양한 응용 프로그램에서 우수한 성능을 제공하는 144개의 명령의 추가로 MMX™ 기술 및 SSE 기술을 확장합니다. SIMD MMX™ 기술 도입된 정수 명령은 64비트에서 128비트로 확장되었으며 SIMD 정수 유형 연산의 유효 실행 속도를 두 배로 늘려줍니다.

두 번 정밀도 부동 소수점 SIMD 명령 SIMD 형식에서 2개의 부동 소수점 연산의 동시 실행 허용 배정밀 연산에 대한 지원 컨텐츠 작성, 재무, 엔지니어링 및 과학 애플리케이션을 가속화하는 데 도움이 됩니다.

원래 SSE 명령어는 또한 다중 데이터 유형(예를 들어, 이중 및 사중 단어)에 대한 산술 연산을 지원함으로써 보다 유연하고 다양한 계산을 지원하는 향상된. SSE2 지침은 소프트웨어 개발자는 최대 허용 알고리즘을 구현하고 성능 향상을 제공할 수 있는 유연성이 MPEG제공됨 - 2와 같은 소프트웨어를 실행하는 경우, MP3, 3d 그래픽 등.

Streaming SIMD Extensions 3(SSE3)

90nm 프로세스 기반 인텔® 펜티엄® 4 프로세서의 출시는 스트리밍 SIMD 확장 3(SSE3)에는 13 SIMD 보다 더 많은 명령을 선보입니다 SSE2. 새로운 13개 명령은 주로 스레드 동기화를 비롯해 미디어 및 게임과 같은 특정 응용 프로그램 영역을 향상시키도록 설계되었습니다.

Streaming SIMD Extensions 4(SSE4)

SSE4 54 명령으로 구성됩니다. 일부 SSE4인텔 문서.1라고 하는 47개의 명령어로 구성된 Penryn에서 사용할 수 있습니다. SSE4.2, 두 번째 부분 집합의 7로 구성된 나머지 지침을 먼저nehalem 기반의 인텔® 코어™ i7 프로세서에서 사용할 수 있습니다. 크레딧을 인텔 개발자들의 피드백 명령어 세트 개발에 있어.



https://www.intel.co.kr/content/www/kr/ko/support/articles/000005779/processors.html

반응형
반응형



아래 그림은 c를 통해 SIMD 연산을 하는 구조를 보여준다






  1. 먼저 32 bit float array[4] 개를 만들어서 128 bit 로 Packing 한다
  2. 이후 패킹된 데이터를 xmm 레지스터에 넣어야 하는데 __m128 a   와 같이 만들어 a 에 넣으면 되는데
    이떄 array 에 있는 값이 a 에 4사이클에 걸쳐서 복사되는 것이 아닌 1 사이클로  array 내용이 a 에 복사되게끔 한다(simd)
  3. 그 이후 sqrt 하나의 명령어를(_mm_sqrt_ps) 실행 하여 1사이클로 4개의 수에 sqrt 를 적용한다
  4. 결과를 result 변수에 저장한다







성능적으로는 C,C++ 보다는 빠른 성능을 보인다


어셈블리어로 SIMD 를 사용하면 성능은 빠르다는 것은 당연한 것인데 

Vector Class 로 가면 갈 수록 약간 느린 경향을 보이긴 하지만 느려지지 않게 잘 파악하면

(SIMD + 어셈)과 (Inteinsic 함수 ) 를 사용하는 것 못지 않게 거의 유사한 성능으로 프로그래밍 할 수 있다

잘만 지키면 3가지 모두 유사한 성능을 보임


하지만 Package 에 Pack 이 몇개 들어가느냐에 따라 성능의 차이는 존재 할 수 있다

어쩌면 당연한 결과.. 


현재 시대에서는 i7 이 많이 보급되어지는 추세라 위 표 보다는 성능이 더 높을 것이라 예상한다(표는 2005~6년 보급형 컴퓨터의 기준이다)







반응형
반응형





128 bit xmm 레지스터에 데이터를 담을때에는 항상 같은 Data type  으로 Pack 해야한다



이게 무슨말이냐면 128 bit 인 xmm 레지스터에 일정한 단위로 데이터를 쌓아야(세팅해야) 한다는 뜻이다


이때 이 쌓는단위를 Pack 이라 하며 모두 쌓았을때 Package 표에 나와 있는 것처럼 128 bit(16 byte) 의 크기어야 하며


한 xmm 레지스터안의 Pack 들의 사이즈는 모두 동일해야 하며 그중 하나라도 다를 경우 오작동한다!





반응형
반응형

SISD - Single Instruction Single Data



SIMD - Single Instruction Multiple Data



SIMD 는 하나의 명령어로 여러개의 데이터를 처리하는 것을 말한다


SISD는 하나의 명령어로 하나의 데이터 처리








예를 들어 c언어에서 두개의 변수를 더하는 명령어 + 는 두개의 변수를 더하는 즉 하나의 명령어로 두 변수를 더해 하나의 변수에 결과를 저장한다

만약 8개의 덧셈을 한다고 한다면 cpu 8 클럭이 지난 후에 8개의 덧셈 연산이 완료 되지만



SIMD 는 1 사이클에 8개의 변수와 상응하는 값을 덧셈연산 할 수 있다


PADDW  는 SIMD 에서 덧셈 연산을 말하며 

MOVDQU XMM0, A 에서의 A 는 short 형 크기 8개가 나열된 배열이라고 보면 된다

이 경우  두개의 변수를 더하게 되면 한사이클로 위 그림처럼 8 개의 덧셈을 처리할 수 있다



이런 연산이 가능한것은 SIMD 를 사용하게 되면 CPU 에서 사용하는 연산을 위해 사용되는 범용 레지스터외에 XMM 이라는 

특수한 레지스터를 사용하기 때문에 이러한 처리속도의 이득을 얻을 수 있다





  • SIMD 는 xmm0~xmm7 번의 레지스터를 사용하며 각 레지스터의 크기는 16byte 이다 (128 bit)
  • 언어는 어셈블리로 SIMD 명령어를 사용하거나 C함수에서 Intrinsic 함수를 사용하거나 C++  에서 Vector Class 를 사용한다





SIMD 는 Intel 과 AMD 에서 사용가능





스트리밍 SIMD 확장

스트리밍 SIMD 확장(Streaming SIMD Extensions, SSE)은 x86 아키텍처에 대한 SIMD(단일 명령 다중 데이터) 명령어 집합 확장이며, 인텔이 1999년에 펜티엄 III 시리즈 프로세서에 도입하였다. 이 기능은 1998년 등장한 AMD사의 3D나우! 기술에 대응한다. SSE는 70가지의 새로운 명령어와 추가적인 레지스터로 구성되며, 명령어의 대부분은 부동 소수점에 대한 연산이다.

SSE는 펜티엄 III가 코드명 Katmai로 알려져 있을 시기에는 KNI(Katmai New Instructions)로 불렸다. 이후 이 이름은 ISSE(Internet Streaming SIMD Extensions)로 정해졌었으며, 이후 SSE로 변경되었다. AMD는 애슬론 XP와 듀론 프로세서를 기점으로 SSE 명령 지원을 추가했다.

레지스터

SSE는 x86 아키텍처에서 XMM0~XMM7의 8개의 128비트 레지스터를 추가한다. 또한 x86-64에서는 XMM8~XMM15의 8개의 레지스터가 추가되었다(단, 이 레지스터는 64비트 모드에서만 사용가능하다). 추가적으로, 32비트 레지스터 MXCSR는 SSE 명령어의 상태 및 제어에 사용된다.

XMM registers.png

SSE는 XMM 레지스터의 자료구조로 4개의 32비트 단정도 부동 소수점을 사용했다. 즉, 하나의 레지스터에 4개의 값이 들어가는 형태였다. SSE에서는 정수 계산을 지원하지 않는다. 이것은 MMX 명령어를 사용하는 방식으로 극복이 가능했다. 또한, SSE2부터는 SSE를 확장하여 다음의 자료구조를 지원한다.

  • 2개의 64비트 배정도 부동 소수점
  • 2개의 64비트 정수
  • 4개의 32비트 정수
  • 8개의 16비트 정수
  • 16개의 8비트 정수

SSE를 처음 지원한 펜티엄 III는 SSE와 FPU를 동시에 사용할 수 없도록 만들어졌다. 이러한 구조는 명령어 파이프라인 효율성을 떨어뜨렸다.

XMM 레지스터는 태스크 스위치 시에 값을 보존해야 하는 대상이기 때문에, 운영 체제가 이 레지스터를 사용하도록 명시적으로 활성화하기 전까지는 사용이 불가능하다. 다시 말하면, 운영 체제가 SSE 레지스터를 보존하는 명령어인 FXSAVE와 FXSTOR를 사용할 수 있어야 한다는 의미이다. 이러한 지원은 주요 IA-32 운영 체제에서 빠르게 추가되었다.

SSE 명령어

  • 부동 소수점 명령어
    • 메모리 대 레지스터 / 레지스터 대 메모리 / 레지스터 대 레지스터 데이터 이동
      • Scalar – MOVSS
      • Packed – MOVAPS, MOVUPS, MOVLPS, MOVHPS, MOVLHPS, MOVHLPS
    • 산술
      • Scalar – ADDSS, SUBSS, MULSS, DIVSS, RCPSS, SQRTSS, MAXSS, MINSS, RSQRTSS
      • Packed – ADDPS, SUBPS, MULPS, DIVPS, RCPPS, SQRTPS, MAXPS, MINPS, RSQRTPS
    • 비교
      • Scalar – CMPSS, COMISS, UCOMISS
      • Packed – CMPPS
    • 데이터 셔플 / 언패킹
      • Packed – SHUFPS, UNPCKHPS, UNPCKLPS
    • 자료형 변환
      • Scalar – CVTSI2SS, CVTSS2SI, CVTTSS2SI
      • Packed – CVTPI2PS, CVTPS2PI, CVTTPS2PI
    • 비트 논리 명령어
      • Packed – ANDPS, ORPS, XORPS, ANDNPS
  • 정수 명령어
    • 산술
      • PMULHUW, PSADBW, PAVGB, PAVGW, PMAXUB, PMINUB, PMAXSW, PMINSW
    • 데이터 이동
      • PEXTRW, PINSRW
    • 기타
      • PMOVMSKB, PSHUFW
  • 다른 명령어
    • MXCSR 관리
      • LDMXCSR, STMXCSR
    • 캐시 및 메모리 관리
      • MOVNTQ, MOVNTPS, MASKMOVQ, PREFETCH0, PREFETCH1, PREFETCH2, PREFETCHNTA, SFENCE

다음 예는 SSE의 장점을 보여준다. 컴퓨터 그래픽등에서 아주 자주 사용하는 명령어인 벡터 더하기를 보자. 두개의 단밀도를 더하기 위해서는 x87를 사용하는 4개의 구성요소 벡터가 4개의 부동소수점 더하기 명령어가 필요하다.

 vec_res.x = v1.x + v2.x;
 vec_res.y = v1.y + v2.y;
 vec_res.z = v1.z + v2.z;
 vec_res.w = v1.w + v2.w;

이것은 오브젝트코드에서 4개의 x87 FADD명령어에 해당한다. 반면에 다음 수도 코드(pseudo-code)에서는 하나의 128비트 ‘packed-add’ 명령어가 4개의 스칼라 더하기 명령어를 대체한다.

 movaps xmm0,address-of-v1          ;xmm0=v1.w | v1.z | v1.y | v1.x
 addps xmm0,address-of-v2           ;xmm0=v1.w+v2.w | v1.z+v2.z | v1.y+v2.y | v1.x+v2.x
 movaps address-of-vec_res,xmm0

뒤에 나온 버전

같이 보기


https://ko.wikipedia.org/wiki/%EC%8A%A4%ED%8A%B8%EB%A6%AC%EB%B0%8D_SIMD_%ED%99%95%EC%9E%A5#SSE_%EB%AA%85%EB%A0%B9%EC%96%B4

반응형

'운영체제 & 병렬처리 > TBB & SIMD' 카테고리의 다른 글

SIMD 연산구조와 성능  (0) 2018.02.09
Pack 과 Package  (0) 2018.02.09
왜 스레딩 빌딩 블록인가  (0) 2012.11.02
TBB 환경설정 threading building blocks  (0) 2012.11.02
[TBB] 기본 알고리즘  (0) 2012.11.02
반응형



CreateThread() 함수는 사용하지 말고 _beginthreadex()함수를 사용하라고하는데 CreateThread() 함수에는 어떤문제가 있는 것인가.




1. CreateThread()

CreateThread() 함수는 CRT 함수가 아닌 WinAPI 함수이다. 또한 이 함수는 멀티스레드가 고려되지 않은 시기에 만들어졌다.

그러므로 당연히 멀티스레드를 사용하는 상황에서 문제가 발생할 수 있다.

예를 들어 strtok() 함수 같은 경우 내부적으로 static 변수를 선언하고 사용하는데, 멀티스레드 환경에서 이러한 전역 변수를 동기화없이 공유하여 사용하게 된다면 문제가 될 수 있다. (물론, 멀티스레드가 고려된 strtok 함수는 내부가 좀 다를것이다.)






2. _beginthreadex()

그래서 나온것이 _beginthreadex() 함수이다. 

_beginthreadex() 함수도 내부적으로 CreateThread() 함수를 호출하지만 그에 앞서서 멀티스레드에서 활용될 독립적인 메모리 블록(_tiddata)을 CRT 힙에 할당한다.





할당한 이 메모리 블럭의 주소는 각 스레드의  TLS에 저장하여 연계시키며, 이는 각종 C/C++ 런타임 라이브러리 함수들의 멀티스레드 문제를 해결하는데 사용된다.


즉, Multi-Threaded 런타임 라이브러리 함수 호출 시 해당 스레드의 tiddata 메모리 블록을 찾아보게 될때 사용되는 것이다.






사실! CreateThread() 함수를 사용한다고 해서 tiddata 블록이 생성되지 않는 것은 아니다.

멀티스레드용 CRT 함수를 호출 시에 해당 스레드의 tiddata블록이 null 이라면 내부적으로 tiddata 블록을 할당하여 사용 하기 때문이다.


하지만, 문제는 CreateThread() 함수를 사용한 사용자가 _endthreadex() 함수를 호출하여 메모리 블럭을 해제하지 않을 것이기 때문에 문제가 발생하는 것이다. (분명 ExitThread() 함수로 종료 시킬것이다.


결국 현재 상황에서는 CreateThread() 함수의 호출은 메모리 누수와 관련이 있다고 볼 수 있는 것이다.





http://chfhrqnfrhc.tistory.com/entry/CreateThread-beginthreadex



다중스레드 사용하기 위한 필수 셋팅

 

 VC++ 로 만든 실행프로그램내에서 스레드를 1개만 사용하는 것이 아닌 경우 VC++ 프로젝트 속성 설정창에서 반드시 다중스레드를 지원하는 C run time library 를 사용하는 것으로 설정이 되어 있어야 한다. 

아래 그림처럼 프로젝트 속성창에서  "다중 스레드 DLL (/MD)" 를 선택해야한다. 프로젝트를 디버그 모드에서 개발중이라면  "다중 스레드 디버그 DLL (/MDd)" 를 선택하면된다.


이름에 DLL 이 붙어있는것은 C런타임 라이브러리를 동적으로 링크하는 설정이고, 이름에 DLL 이 붙어있지 않은 것은 정적으로 링크 한다는 의미이고, 이름에 dll이 붙어있는것은 동적으로 링크 한다는 설정인데, 이 설정은 프로젝트의 "속성  -> 일반 -> MFC 사용" 을 "정적 라이브러리에서 MFC사용" 으로 한 경우에는 런타임 라이브러리는  "다중스레드 (/MT)" 를 선택해야하고, "MFC사용" 선택을 동적  MFC사용으로 한경우에는 본 런타임 라이브러리에서는 "다중 스레드 DLL (/MD)" 를 선택해야한다. 이것 짝이 맞지 않은 경우에는 컴파일시에 에러가 나오므로 실수할 걱정은 하지 않아도 된다.

 

만일 아래 옵션이 단일스레드로 설정된 상태에서 _beginthreadex 나 _beginthread 를 사용하면 컴파일시 에러가 발생한다.






 http://igotit.tistory.com/entry/스레드-생성-방법-및-다중스레드-사용위한-필수-옵션-셋팅-VC [igotit]






2. 멀티 스레드 안전한 C/C++ Library
  역사적으로 C runtime-library 개발자는 멀티 스레드 어플리케이션에서 C runtime-library 를
사용하였을 때 발생하는 문제에 대해서는 전혀 고려하지 않았다. 멀티 스레드 어플리케이션
에서 전통적인 C runtime-library 를 사용하였을 때 문제가 발생할 수 있다.
따라서 Microsoft 는 이러한  문제를 해결하기 위해서 스레드 안전한 C/C++ runtime-library 를
제공하고 있다.
멀티 스레드 안전한 C/C++ run-time library 함수는 다른 스레들로부터 영향을 받지 않도록
자신을 호출한 스레드의 데이터 블록에만 접근 가능하게 한다.


3. Single-thread C/C++Library 와 Multi-thread C/C++ Library
Single-thread C/C++Library 는 단일 스레드 전용의 함수들을 말하고,
Multi-thread C/C++ Library 는 멀티 스레드 전용의 함수들을 말한다.

   ※ Visual Studio 2008 에서는 Multi-thread C/C++ Library 만 지원한다. 
       즉 더 이상 단일 스레드 전용의 C/C++ 라이브러리는 제공하지 않는다.



http://egloos.zum.com/heilow/v/83533

반응형
반응형

리눅스에서의 가상 메모리란?

윈도우랑 거의 같다

가상 메모리란?

리눅스는 가상 메모리(virtual memory)란 것을 지원한다. 이것은 메모리 사용량이 늘어남에 따라,

디스크의 일부를 마치 확장된 RAM처럼 사용할 수 있게 해주는 기술이다.

이 기술에 따르면, 커널은 실제 메모리(RAM)에 올라와 있는 메모리 블록들 중에 당장 쓰이지 않는 것을 디스크에 저장하는데,

이를 통해 사용가능한 메모리 영역을 훨씬 늘릴 수 있게 된다.


만일 디스크에 저장되었던 메모리 블록이 다시 필요하게 되면 그것은 다시 실제 메모리 안으로 올려지며, 대신 다른 블록이 디스크로 내려가게 된다.


그러나 이런 과정이 일어나고 있다는 것이 사용자에게는 전혀 보이지 않으며, 프로그램들에게도 그저 많은 양의 메모리가 있는 것처럼 보일 뿐이어서,

점유하고 있는 메모리가 디스크에 있는지 실제 메모리에 있는지 전혀 신경쓸 필요가 없게 된다.


그러나, 하드디스크를 읽고 쓰는 시간은 RAM보다 훨씬 느리기 때문에(보통 천배쯤 느리다), 프로그램의 실행은 그만큼 더디게 된다.

이렇듯 가상적인 메모리로 쓰이는 하드디스크의 영역을 `스왑 영역(swap space)'이라고 한다(swap은 바꿔치기를 한다는 뜻).


리눅스스왑 영역으로 일반적인 파일을 사용할 수도 있고 별도의 스왑을 위한 파티션을 사용할 수도 있다.

스왑 파티션은 속도가 빠른 반면에, 스왑 파일은 그 크기를 자유롭게 조절할 수 있다

(또한 스왑 파일을 사용하면, 리눅스 설치시에 파티션을 다시 해야 할 필요없이 모든 것을 그냥 설치할 수 있다).


스왑 영역이 얼마나 많이 필요한지를 미리 알고 있다면 그만큼 스왑 파티션을 잡으면 된다.

그러나 스왑 영역이 얼마나 필요할지 확실히 모른다면, 우선 스왑 파일을 사용해서 시스템을 가동해 보고

필요한 공간이 얼마인지 파악한 후에 스왑 파티션을 잡도록 하자.


또한 리눅스에서는 여러개의 스왑 파티션과 스왑 파일을 섞어서 사용할 수 있다.

이 방법을 이용하면, 언제나 큰 용량의 스왑 영역을 잡을 필요없이 그때 그때 필요한 만큼만 스왑을 늘려줄 수 있으므로 편리하다.


운영체제 용어에 관한 이야기 : 컴퓨터 과학에서는 스와핑(해당 프로세스 전체를 스왑 영역으로 내보냄)과 페이징(몇 킬로바이트의 작은 단위로 내보냄)을

구별하는 것이 일반적이다. 이 중에서 페이징이 좀더 효율적인 방법이며, 리눅스에서도 이 방법을 쓴다.

그러나 전통적인 리눅스 용어로는 이 두가지를 모두 뭉뚱그려서 스와핑이라고 흔히 불러왔다.


https://wiki.kldp.org/Translations/html/SysAdminGuide-KLDP/memory-management.html

반응형
반응형


기본적으로 멀티 프로그래밍(멀티 태스킹)을 지원하는 OS에서는 여러개의 프로세스(프로그램)이 함께 돌아가야 한다. 

즉 한 프로세스가 일방적으로 CPU를 독점하는 것이 아니라, 여러개의 프로세스가 잠깐씩 번갈아 실행되어 사용자 입장에서는

동시에 여러 프로세스가 돌아가는 것 처럼 느끼게 하는 것이다.

이 과정에서 필수적으로 프로세스 간의 문맥 전환(Context Switching)이 일어나고 문맥 전환에는 많은 비용이 든다.

가령 A라는 프로세스에서 B라는 프로세스로 CPU의 실행 권한이 넘어갈 때 , 다음 번 A의 실행이 재개되었을 때

실행의 연속성을 보장하기 위해 A 프로세스의 많은 정보를 어딘가에 보관해야 한다. (첫 번째로 메모리에 저장 시도)

 

만일 메모리가 무척 커서 A와 B 모든 코드나 데이터, 그리고 레지스터 값의 백업 데이터 등을 다 저장하고도 남는다면,

이 과정이 비교적 빠르게 진행될 수 있을 것이다. 하지만 불행히도 메모리가 두 프로세스 중 딱 하나의 정보만 수용할 수 있을 정도로

작다면, OS는 문맥 전환과 함께 스와핑(Swapping)이라는 과정을 거쳐야 한다.

 

스와핑이란 A프로세스의 문맥 정보, 즉 코드나 데이터, 레지스터 백업 값 등을 메모리에서 보조 기억장치(주로 하드디스크)로

옮겨둔 다음, 새로 실행이 재개되는  B프로세스의 문맥 정보들을 다시 하드디스크에서 읽어오는 것을 말한다.

하드디스크로 이런 프로세스의 문맥 정보를 옮기는 것을 Roll Out(롤 아웃), 반대로 재개되는 프로세스의 문맥 정보를 읽어서

메모리로 가져오는 것을 Roll In(롤 인)이라고 한다. 

이런 스와핑을 지원하는 OS라면 몇 개의 프로세스가 있더라도 이론적으로는 아무 문제없이 돌릴 수 있다.

롤인과 롤 아웃을 하는데 시간이 얼마나 걸리는지 굳이 따져보지 않더라도 경험적으로 충분히 문맥 전환 간격보다는 훨씬 클 거라 예상할 수 있다.

결국 그런 이유로 대부분의 시간을 스와핑하는데 다 쏟아 붓고 정작 프로그램을 돌리는 데에는 아주 적은 시간밖에 할애할 수 밖에 없는

아이러니한 상황이 된다.

 

이를 해결하기 위한 가장 쉬운 해결책은 대용량의 메모리를 사용하는 것이다.

그리고 한 프로세스가 모든 메모리를 차지하는 것이 아니라, 메모리가 가득 찰 때 까지 가능한 모든 프로세스를 다 메모리에 보관하고

스와핑 하지 않는 것이다.

메모리가 풀이 난 시점부터는 스와핑을 통해 문맥 전환을 할 수 있기도 하겠지만, 결국 문맥 전환할 때 마다 프로세스 단위로 롤인, 롤 아웃을

반복해야 한다면 다시 앞서의 배보다 배꼽이 더 커지는 문제점이 그대로 발생할 것이다.

 

이런 방식을 다중 분할 할당(Multiple-Partition Allocation) 이라 부른다.



만일 프로세스 2가 끝나기 전에 프로세스 4가 실행되려 한다면 앞의 프로세스 4는 자신의 크기만한 공간이 확보될 때까지,

즉 앞의 프로세스 중 누군가가 종료되어 공간이 날 때 까지 기다려야 할 것이다.

그나마 다행히 프로세스 2나 3이 종ㄹ되면 충분한 공간이 생기지만 프로세스 1은 종료되어 봐야 겨우 100바이트의 공간만

생길 뿐 200바이트 크기인 프로세스 4가 실행되기엔 역부족이다.

 

이런 문제를 단편화(Fragmentation)라 부른다.

즉 메모리 끝에 남아있던 100바이트와 프로세스 1이 종료되면서 생긴 100바이트를 합치면 200바이트니까 프로세스 4를 돌리기에

충분할 것 같지만, 두 공간이 나뉘어 있어서 실질적으론 쓸모없는 공간이라는 것이다.

이런 식의 단편화 문제를 뒤에서 보게 될 또 다른 형태의 단편화라는 것과 구분하기 위해

외부 단편화 라고 부른다. (External Fragmentation)

 

이러한 외부 단편화 문제를 해결하기 위해서 빈 공간을 활용하는 몇 가지 방법이 알려져 있고, 아예 자잘한 빈 공간들을 몰아서

큰 공간을 만드는 압축(Compaction)이라 불리는 방법이 있다.

빈 공간을 찾는 방법으로는 가장 먼저 발견되는 큰 공간을 사용할 것인지, 아니면 전체 빈 공간중에 새 프로세스의 크기와 가장 비슷한

곳을 활용하는 등의 몇 가지 방법이 있다. 이들은 실험적으로 어느 방법이 더 효율이 좋다 나쁘다 하는 것들이 알려져 있으나

이런 방식의 다중분할할당 자체가 그리 선호되는 방법이 아니다.

 

압축 역시 가장 확실한 방법이기는 하지만 현재 메모리상에 있는 프로세스들을 옮겨 연속되도록 붙여야 하는데,

앞에서 살펴본 컴파일 타임 혹은 적재 시간 주소 바인딩으로는 이를 해결하기 어렵다.

왜냐하면 두 방법 모두 프로세스가 실행되기 전에 메모리를 참조하는 인스트럭션들의 타겟 주소값이 모두 결정나버려

실행중에 있는 프로세스들이 옮겨지면 엉뚱한 메모리를 참조하게 되기 때문이다.


왼쪽 이미지에서 프로세스 1과 printf 함수 사이에는 빈공간이다 이것이 오른쪽 그림에서 압축 되면서 비어있던 공간이 아래로 배치됨

 


그래서 필요한 것이 바로 실행 시간 주소 바인딩(Execution Time Address Binding)이다.

실행시간 주소 바인딩에서는 call과 같은 인스트럭션의 타겟 메모리 주소가

실제 그 인스트럭션이 CPU에 의해 패치되기바로 직전에 결정나게 하는 방식이다.

이를 구현 하기 위해 컴파일러(혹은 링커)는 call printf부분의 코드를 생성할 때 이 프로세스가 항상 0번지를 기준으로

시작된다고 전제하고 타겟 주소를 생성한다. 그러면 printf루틴이 최종 컴파일 결과 코드의 제일 첫 부분에 위치하게 되었으므로

이를 호출하는 call printf 부분이 call 0x0로 기술될 것이다. 그리고 실제 이 코드가 CPU로 패치되는 시점에서

비로소 프로세스 2가 메모리에 위치한 시작 주소와 함께 더해져 최종 주소가 결정되는 것이다.


왼쪽 이미지에서 프로세스 1과 printf 함수 사이에는 빈공간이다 이것이 오른쪽 그림에서 압축 되면서 비어있던 공간이 아래로 배치됨



사실 이러한 실행시간 주소 바인딩은 (혹은 실시간 주소 바인딩) OS의 힘만으로는 구현하기 힘들다.

OS가 이를 해결하려면 매 인스트럭션 수행 때마다 OS로 다시 제어권이 넘어가 수행될 다음 인스트럭션의 주소 바인딩을 위해

인스트럭션을 수정해서 다시 메모리에 저장해야 하는데, 그 경우 속도가 어마어마하게 떨어지게 된다.

따라서 이런 경우 하드웨어 적인 지원이 필수적인데 최근의 고성능 CPU들은 대부분 이런 실행시간 주소 바인딩을 지원하기 위해

별도의 메커니즘을 내장하고 있다. 한 가지 간단한 예로 메모리를 참조하는 인스트럭션은 CPU가 읽어갈 때

그냥 읽어가는 것이 아니라 타겟 주소를 항상 특정한 레지스터 값과 더해서 읽는 것이다. (이 레지스터를 Relocation Register 라고 한다)

그러면 각 프로세스는 실행 될 때 필요한 자신의 시작 주소를 문맥이 전환될 때 마다 재위치 레지스터에 저장해 놓은 것 만으로

모든 메모리 주소를 참조하는 인스트럭션이 정상적인 메모리를 참조할 수 있다.


(call print 주소이동을 상대적으로 처리하겠다는 것)

 

하지만 압축을 한다 하더라도, 압축을 어느 시점에 해야할 지 고민이고

또 항상 압축된 상태로 있다해도 결국엔 프로세스 수가 늘어남에 따라 메모리가 가득차는 일이 벌어지고 말 것이다.

 

결국 다른 대안인 페이징 이라는 혁신적인 방법으로 해결 할 수 있다.

 


정보 출처 - 뇌를 자극하는 프로그래밍 원리 (CPU부터 OS까지) - 한세경 저


http://blog.naver.com/PostView.nhn?blogId=cnfldidhd&logNo=20171653078

 


반응형
반응형

캐시 메모리




1. 개요2. 배경3. 작동 원리4. 구조와 작동 방식5. 역사6. 유사한 것

1. 개요[편집]

Cache memory.

컴퓨터 시스템의 성능을 향상시키기 위해 주로 CPU 칩 안에 포함되는 빠르고 작고 비싼 메모리이다. 프로그램에서 직접적으로 읽거나 쓸 수 없고 하드웨어의 메모리 관리 시스템이 내부적으로 제어한다. 대부분 프로그램은 한 번 사용한 데이터를 다시 사용할 가능성이 높고, 그 주변의 데이터도 곧 사용할 가능성이 높은 데이터 지역성을 가지고 있다. 데이터 지역성을 활용하여 캐시보다는 느리지만 용량이 큰 메인 메모리에 있는 데이터를 캐시 메모리에 불러와 두고, CPU가 필요한 데이터를 캐시에서 먼저 찾도록 하면 시스템 성능을 향상시킬 수 있다.

흔히 1단계 캐시(L1 캐시)와 레지스터를 혼동하는데 완전히 다른 개념이다. L1 캐시에 있는 데이터도 궁극적으로는 레지스터에 올라가야 CPU에서 처리할 수 있으며, 레지스터 내 데이터는 프로그램 코드에서 직접 제어할 수 있다. 메모리 형태를 가지고 있는 L1 캐시와는 다르게 레지스터는 CPU 아키텍처에 따라서 종류와 의미가 다르다.

또한 가상메모리에서는 DRAM이 디스크의 캐시로서 동작한다.

2. 배경[편집]

CPU의 클럭 속도가 매우 빨라짐에 따라 CPU 밖에 있는 DRAM과의 속도 차이가 현저하게 증가하였는데, 이 때문에 CPU 클럭 속도를 아무리 올려도 DRAM에서 데이터를 빠르게 제공해 주지 못하여 전체 시스템 성능이 증가하기 어렵게 되었다. 메모리 기술은 주로 DRAM 기술, SRAM 기술로 나뉘는데, DRAM은 가격은 싸지만 속도가 느리고, SRAM은 속도는 빠르지만 가격이 비싸다는 단점이 있었다. 그래서 SRAM을 사용자가 직접 장착하게 하는 대신, CPU와 DRAM 사이에 SRAM을 별도로 두어서 DRAM의 데이터를 직접 접근하는 것보다는 빠르게 접근할 수 있도록 했다. 여기에 사용하는 SRAM을 캐시 메모리라고 한다. 폰 노이만 은 1946년 그의 논문 "Preliminary Discussion of the Logical Design of an Electronic Computing Instrument" 에서 캐시 메모리의 필요성을 예견했다. 역시 굇수 혼자 다 해먹네

3. 작동 원리[편집]

캐시 메모리는 데이터 지역성(Locality)의 원리를 사용한다. 데이터 지역성은 대표적으로 시간 지역성(Temporal locality)과 공간 지역성(Spatial Locality)으로 나뉘는데, 시간 지역성이란 for나 while 같은 반복문에 사용하는 조건 변수처럼 한 번 참조된 데이터는 잠시 후에 또 참조될 가능성이 높다는 것이고, 공간 지역성이란 A[0], A[1]과 같은 데이터 배열에 연속으로 접근할 때 참조된 데이터 근처에 있는 데이터가 잠시 후에 사용될 가능성이 높다는 것이다.

쉽게 예를 들자면 무지하게 지랄맞고 부지런한 상사가 2010년 재무결산 보고서를 가져오라고 했을 때, 무슨 일인지는 몰라도 또 가져오라고 할지도 모르니까 2010년 재무결산보고서를 일단 준비해 놓고, 2009년이나 2011년, 2012년 재무결산보고서도 가져오라고 할지 모르니까 그것도 준비해 놓는 식이다.

또 다른 예로는 캐시는 지갑이라고 생각하면 된다. 지갑 혹은 주머니가 없다면 우리가 현금이 필요할 때마다 매번 은행이나 ATM에 가야 할 것이다. 이는 당연히 매우 귀찮고 시간도 많이 걸린다. 하지만 우리가 현금을 지갑에 넣고 다님으로써 시간을 절약할 수 있다.

CPU가 메모리에 데이터를 요청할 때, DRAM에 접근하기 전에 일단 캐시 메모리에 접근하여 데이터 존재 여부를 확인한다. 캐시 메모리는 메인 메모리인 DRAM보다는 그 사이즈가 매우 작아서 데이터를 모두 저장할 수 없다. DRAM이 보통 4~16GB 정도인데 인텔 i5, i7에 들어가는 캐시 메모리는 32KB ~ 8MB 정도이다. 캐시 메모리는 DRAM의 데이터 일부를 가지고 있다가 CPU가 요청한 데이터가 캐시 메모리에 없으면 CPU를 잠시 기다리게 한 후 DRAM에서 해당 데이터를 가져온 후 CPU에게 넘겨 준다. CPU는 캐시의 존재를 알고 있지만, 그 위에서 실행되는 프로그램은 메모리 주소만 지정할 수 있지 프로그래머가 캐시를 직접 지정할 수는 없다. 이렇게 그 존재가 외부에 드러나지 않기 때문에 캐시 메모리는 CPU에 투명(transparent)하다고 한다. 투명하지 않은 작은 온칩 메모리는 Scratchpad Memory라고 부른다.

캐시에 데이터를 저장할 때 공간 지역성을 최대한 활용하기 위해 해당 데이터뿐만 아니라 옆 주소의 데이터도 같이 가져와 미래에 쓰일 것을 대비한다. DRAM에는 프로그램을 수행하는 명령어(Instruction)와 그 명령이 실행되는 데이터(Data)가 함께 들어 있는데, 명령어는 읽기만 하고 데이터는 읽기와 쓰기를 동시에 하므로 캐시 메모리 내에 이들을 각각 I-Cache(Instruction Cache)와 D-Cache(Data Cache)에 저장한다. 보통 L1 캐시에는 I-Cache와 D-Cache가 따로 있고, L2 캐시는 딱히 둘의 구분 없이 하나의 캐시 메모리로 구성된다. L1 캐시는 CPU에 직접 데이터를 공급해 주기 때문에 빠른 접근 지연 시간(Access latency)이 매우 중요한데, 명령어는 보통 공간 지역성이 높고 데이터는 보통 시간 지역성이 높다. 이 둘을 나누어 서로 다른 지역성을 이용할 수 있다. 또한 명령어와 데이터를 동시에 읽어올 수 있게 함으로써 CPU의 파이프라이닝 성능을 향상시킬 수 있다.

CPU가 데이터를 요청했을 때 캐시 메모리가 해당 데이터를 가지고 있다면 이를 캐시 히트라 부르고, 해당 데이터가 없어서 DRAM에서 가져와야 한다면 캐시 미스라 부른다. 

캐시 미스 발생시의 처리 방법은 캐시 정책에 따라 다르며, 데이터를 읽어 오는 시점으로 사용하기도 한다.

캐시 미스가 나는 경우는 대부분의 경우 3가지로 나눌 수 있는데

  • Compulsory miss(또는 cold miss) : 해당 메모리 주소를 처음 불렀기 때문에 나는 미스. 예를 들어 프로그램을 새로 켜거나 하는 경우 발생한다. 간혹 사용할 데이터를 미리 프리페치하는 경우가 아닌 이상 사실상 예방이 불가능한 캐시미스지만, 전체 컴퓨터 이용 시간에 비하면 굉장히 드물게 나는 미스 유형이라 전체적인 성능에 영향을 미치는 정도는 작다.

  • Conflict miss : 캐시 메모리에 A 데이터와 B 데이터를 저장해야 하는데, A와 B가 같은 캐시 메모리 주소에 할당되어서 나는 캐시 미스다. 예를 들어 내가 휴대폰과 따뜻한 커피캔은 항상 외투 오른쪽 주머니에만 넣는 습관이 있다고 하자. 평상시에는 오른쪽 주머니에 휴대폰만 넣고 다니는데, 어느날 친구에게 커피캔을 받아서 잠시 휴대폰을 가방 속에 넣어두고 커피캔을 오른쪽 주머니에 넣었다. 이때 휴대폰을 오른쪽 주머니에서 찾으려고 한다면 그때 conflict miss가 난다. direct mapped cache에서 가장 발생빈도가 높고, n-associative cache에서 n이 커질수록 발생빈도가 낮아지지만 대신 n이 커질수록 캐시 속도가 느려지고 파워도 많이 먹는다.

  • Capacity miss : 캐시 메모리에 공간이 부족해서 나는 캐시 미스. 위의 conflict miss는 캐시에 공간이 남아도는데도 불구하고 주소 할당때문에 나는 미스지만, capacity miss는 주소 할당이 잘 되어있더라도 공간이 부족하면 나는 미스다. 캐시 공간이 작아서 벌어지는 일이므로 캐시 크기를 키우면 해결되지만, 캐시 크기를 키우면 캐시 접근속도가 느려지고 파워를 많이 먹는다는 단점이 생긴다.


만약 대부분의 메모리 요청이 캐시 미스라면 캐시 메모리를 안 쓰는 게 더 빠르다. 하지만 다행히도 캐시 미스 비율이 대체로 평균 10% 안쪽이기 때문에 캐시 메모리를 통해 컴퓨터 시스템의 평균 성능을 크게 향상시킬 수 있으며 클럭 속도, 코어 개수와 함께 컴퓨터 성능에서 매우 큰 비중을 차지한다. 그러나 많은 사람들이 캐시 메모리에 대해 잘 모르며 이름부터 생소하니까 실제 캐시 메모리가 없이 클럭 속도가 더 높은 CPU가 클럭 속도는 낮지만 캐시 메모리가 있는 CPU보다 대체로 더 나쁜 성능을 보여준다. 셀러론이 싼데는 다 이유가

어떤 이는 컴퓨터에 사용된 도박의 원리라고 하기도 하는데, 이는 캐시 메모리의 작동 원리가 도박에서 돈을 거는 것과 유사하기 때문이다. 캐시 미스 비율 (질 확률)이 매우 낮아 거의 항상 이기는 도박이라 할 수 있다.

4. 구조와 작동 방식[편집]

  • Direct Mapped Cache


  • 가장 기본적인 캐시 구조. DRAM의 여러 주소가 캐시 메모리의 한 주소에 대응되는 다대일(n:1) 방식이다. 그림을 참고해서 간단히 설명하자면, 메모리의 공간이 32개(00000₂부터 11111₂)이고, 캐시 메모리의 공간이 8개(000₂부터 111₂)이면 메모리의 주소가 00000₂, 01000₂, 10000₂, 11000₂인 곳의 데이터를 캐시 메모리의 주소가 000₂인 곳에만 읽고 쓰는 방식이다. 이때 000₂을 인덱스 필드(Index Field), 인덱스를 제외한 나머지 비트 00₂, 01₂, 10₂, 11₂을 태그 필드(Tag Field)라 하고, 기본적으로 태그 비트와 인덱스 비트 뒤에 데이터를 저장하는 데이터 필드(Data Field)가 붙어있는 구조를 갖고 있다.


Direct Mapped Cache의 작동 방식은 간단하지않게 예를 들어, 캐시 메모리가 비워진 상태로 CPU가 메인 메모리의 00000₂ 주소의 데이터를 두 번 연속해서 읽는다고 하자. CPU는 먼저 캐시 메모리를 뒤적이는데 요청한 주소가 00000₂이므로 캐시 메모리의 주소가 000₂인 곳을 확인한다. 확인 후 아무것도 없는 것을 확인하고 직접 메인 메모리의 00000₂주소를 참조하여 데이터를 읽어온다. 이때 또 쓸지도 모르니까 캐시 메모리의 000₂인 공간에 00₂태그비트와 아까 읽은 메인 메모리 00000₂의 데이터를 저장해 놓는다. 그 다음 바로 같은 동작을 수행하면서 똑같이 캐시 메모리의 000₂ 인 곳을 확인하는데 이번엔 데이터가 들어있으니 그 데이터가 CPU가 요구한 주소의 데이터와 일치하는지 태그 비트를 비교하는 검사를 한 뒤 태그 비트마저 일치하면 캐시 메모리에서 데이터를 가져온다.

  • Set Associative Cache

  • Fully Associative Cache

5. 역사[편집]

초창기 캐시 메모리는 메인보드에 붙어서 CPU와 메인 메모리 사이에 존재했으나, CPU 집적도가 높아지면서 CPU 안에 캐시 메모리가 포함되게 되었다 오오 무어. 펜티엄용 메인보드는 CPU 근처에 별도의 캐시 메모리 슬롯이 있어서 필요한 경우에 L2 캐시 메모리를 증설할 수도 있었다. CPU 외부에 별도 장착되는 캐시 메모리는 CPU 속도와는 별개로 작동했기 때문에, 캐시 메모리 속도가 시스템 속도에 영향을 주었다.

펜티엄 이후 CPU에서 L2 이상의 캐시를 CPU 칩 패키징 내에 넣으려는 시도가 있었고, x86 아키텍처 중 인텔에서는 1995년 11월 출시된 펜티엄 프로에서, AMD에서는 1999년 출시된 K6-III에서 최초로 실현되었다. L2 캐시가 CPU 내에 통합되기 시작한 시기에는 캐시 메모리가 실장된 형태가 On-die, On-package로 구분되어 있었다. On-die 캐시는 하나의 다이 안에 CPU 코어와 L2 캐시가 함께 포함되어 있는 것이고, On-package 및 그와 유사한 용어는 CPU 코어와 L2 캐시 다이는 서로 나뉘어 있지만 하나의 패키징된 칩 안에서 서로 연결된 형태를 가리킨다.

인텔은 펜티엄 프로에서 하나의 칩 안에 CPU 코어와 L2 캐시 다이를 모두 올리는 형태를 사용했다. 그러나 가격 문제 때문에 일반 사용자를 대상으로 한 펜티엄 2와 초기 펜티엄 3 카트마이 및 셀러론 모델에서는 CPU 코어와 L2 캐시를 별도 칩으로 분리했고, CPU 형태도 소켓에서 슬롯으로 바꾸었다. 슬롯 1이 사용되던 시기의 인텔 CPU에서는 CPU 코어와 별개로 외부 제조사에서 만든 캐시용 SRAM이 붙어 있었고, 셀러론은 캐시 메모리가 붙어 있지 않거나 용량이 더 작았다. AMD는 K6-III에서 On-die L2 캐시를 도입했으나, 인텔과 같은 이유로 인해서 초기형 애슬론은 슬롯 형태로 전환했다. 그러나 기술이 발전하여 On-die 캐시를 만들기 쉬워졌고, 외부 캐시 메모리의 속도를 끌어 올리는 데에는 한계가 있기 때문에 인텔은 펜티엄 3 코퍼마인, AMD는 애슬론 선더버드부터 On-die L2 캐시로 복귀했다. 슬롯이 다시 소켓으로 돌아간 것은 보너스.

인텔은 초창기의 셀러론에서 L2 캐시를 없앴다가 엄청난 성능 저하를 보였다. 이후 인텔은 아무리 저가형 모델이라고 하더라도 L2 캐시는 용량이 작더라도 항상 달고 나오기 시작했다. 2015년 시점에서는 최소 2단계, 보통 3단계까지 존재한다. 3단계 캐시가 있는 CPU는 인텔은 펜티엄 4 Extreme Edition[1]인텔 코어 i 시리즈가 있으며 AMD는 AMD 페넘 시리즈에서 최초로 L3 캐시를 도입한 후, AMD 페넘 II 시리즈AMD FX 시리즈 등이 있다. 당연히 단계가 내려갈수록 속도는 상대적으로 떨어지게 되며, 반대급부로 용량은 증가한다. 물론 코어2 쿼드 같이 2단계 캐시가 12MB나 달린 깡패가 있긴 하다 인텔의 브로드웰에 와서는 4단계 캐시인 128MB eDRAM이 적용되었다. 이는 주로 내장그래픽 칩셋에서 사용한다. 속도가 빠르고 대역폭도 넓다보니 기존의 HD시리즈와는 차원이 다른 성능을 보여줬다.

6. 유사한 것[편집]

하드 디스크 사이의 속도 향상을 위해서 보통 16MB~64MB 정도의 메모리를 달아서 좀 더 빠른 입출력이 가능하도록 지원한다. 이를 '버퍼' 또는 '버퍼 메모리'라고 부르는데 캐시 메모리와 완전히 같진 않지만 거의 유사한 방식으로 동작한다. 

SSD에도 동일한 개념의 버퍼 메모리가 달려 있다. SSHD의 경우 하드 디스크의 버퍼를 아예 저용량의 SSD랑 섞어버린 것.

메인 메모리의 일부를 떼어서 HDD/SSD의 데이터를 캐싱하는것도 가능하며 이를 '디스크 캐시'라고 한다. DOS 시절 1M 메모리가 달린 286 이상의 컴퓨터에는 640kiB를 넘어가는 상위 메모리 영역의 일부를 디스크 캐시로 사용할 수 있도록 해 주는 유틸리티가 여러 종류 있었다. 이후 Windows 95로 넘어오면서 운영체제에서 자체적으로 디스크 캐시를 제공하게 되었다. Linux 등의 유닉스 계열 운영체제도 디스크 캐시를 기본 기능으로 보유하고 있다.



출처 : https://namu.wiki/w/%EC%BA%90%EC%8B%9C%20%EB%A9%94%EB%AA%A8%EB%A6%AC



반응형
반응형





http://blackrain.egloos.com/1224982

글 본문에 대한 내 생각을 밝히는 것이 아니기 때문에 트랙백은 남기지 않았다. 다만 글 쓰신 Gony님이 인용하신 '조엘 온 소프트웨어'의 대목에 많은 오류가 있어서 이렇게 글을 적어본다. 물론 좁은 지면과 독자의 배경 지식을 감안해서 쉽고 간단하게 malloc() 동작 원리를 썼으리라 생각한다. 그러나 실제 방식은 이와 많이 다르다. 혹시나 오해를 가질까 해서 이 글을 쓴다.

malloc이 어떻게 동작하는지 아십니까? malloc의 본질은 사용 가능한 메모리 블록을 연결 리스트linked list로 길게 연결한 자유 체인(free chain)입니다. malloc은 연결리스트를 따라가며, 요청 받은 메모리 양보다 큰 블록을 찾습니다. 이렇게 찾은 블록을 2개로 쪼개서, 하나는 호출한 사용자에게 반환하며, 쪼개고 난 다음에 남아있는 블록은 다시 연결 리스트에 넣어 둡니다. free를 호출할 때, free는 해제한 메모리를 자유 체인에 추가합니다. 결국 자유 체인은 자그마한 조각으로 잘게 쪼개지므로, 큰 조각을 요청하면 원하는 크기를 충족하는 조각이 없을 수도 있습니다. 이럴 경우 malloc이 타임아웃을 선언한 다음에 자유 체인 주위를 샅샅이 훑어 조각을 정렬하고 인접한 작은 자유 블록을 더 큰 블록으로 결합합니다. 이런 작업을 하다 보면 밤이 샐 것입니다. 결국 이 모든 혼란의 끝은 malloc의 성능 특성이 결코 아주 빠르다고 볼 수 없으며(malloc은 항상 자유 체인을 돌아다닙니다), 정리하는 과정에서 종종 예측할 수 없을 정도로 지독하게 느려질 수 있다는 사실로 귀결됩니다.

덧붙여 말하자면, 이런 현상은 가비지 컬렉션을 지원하는 시스템과 동일한 성능 특성이며, 시스템 성능 저하 원인이 가비지 컬렉션(garbage collection)이라는 주장이 전적으로 옳지만은 않다는 놀라운 결론에 도달합니다. 비록 정도는 약하지만, 전형적인 malloc구현을 따를 경우에도 가비지 컬렉션과 유사한 성능저하 문제가 생기니까요.

아 길다. 이걸 직접 타자하신 것 같은 Gony님께 감사의 말씀을 전한다 (띄어쓰기는 손을 좀 봤습니다). malloc의 작동 원리는 보기에는 간단하지만 실제로는 수 많은 최적화가 되어있어서 상당히 복잡하다. malloc 구현은 사실 여러 가지 버전이 있다. 대표적으로 리눅스의 libc에서 사용되는 Doug Lea의 dlmalloc이 있다. 이 외에도 Solaris 플랫폼의 버전도 있고 Win32 Heap 관리 알고리즘도 있다. 약간의 미세한 차이는 있지만 큰 틀은 차이가 없다. 내가 지적하는 것은 모두 Doug Lea의 malloc 구현 (버전 2.8.3)을 바탕으로 이야기 하는 것이다. 

malloc구현은 일단 큰 메모리를 시스템으로부터 얻어오는 것으로 시작한다. 리눅스라면 brk/sbrk가 있고, Win32라면 VirtualAlloc이 있다. 이들 함수가 user-level에서 사용할 수 있는 가장 원초적인 메모리 할당 함수들이다. 그러나 이들 메모리 할당 함수들은 그 할당 단위가 크다. 보통 페이지 단위이기 때문에 4KB 정도이다. 그리고 시작 메모리도 이 페이지 크기에 정렬이 되어있어야 하므로, 작은 메모리를 동적으로 할당 받고 쓰기에는 사실 상 불가능하다. 그래서 이렇게 크게 얻어온 메모리 덩어리를 잘게 썰어주고 관리하는 녀석들이 heap management library이다. 그리고 이 malloc 구현은 20년 가까이 된 것이고 속도와 메모리에 상당히 많은 최적화가 되어있다. 속도 보다는 메모리에 보다 더 많은 관심을 기울였다고 볼 수 있다.

또, malloc의 할당 단위는 보통 8byte이고 최소 할당 크기는 16byte 정도 된다. (시스템마다 32/64비트 마다 다를 수 있다). 그러니까 malloc(14)와 malloc(15)를 호출해도 부가 정보를 기록하기 위한 8바이트를 더해 다음 8바이트 단위로 align 시켜서 똑같이 24바이트가 할당 된다. 

예를 들어, malloc(16)을 호출해서 100번지의 주소를 반환 받았다면, 92번지에 현재 이 노드가 사용 중인가, 얼마나 할당 하였는가, 혹은 그 전 노드의 크기가 얼마인지를 저장한다. 이러한 노드는 'chunk'라는 구조체로 표현이 되는데 사실 이 보다 더 복잡하다. 비록 8바이트를 metadata를 위해 쓰지만 앞의 chunk가 사용 중이라면 맨 앞 4바이트는 앞 chunk의 데이터로 채워질 수 있다. 그러니까 겹쳐있는 부분이 있다. 메모리를 조금이라도 아끼기 위해 이런 방식을 취했는데 처음 소스 보면 이해하기 쉽지가 않다.

Malloc이 처음 나왔을 당시에는 그 누구도 이걸 악용해서 컴퓨터 제어권을 탈취하는 상황을 생각해 본 사람이 없다. 그래서 보안에는 사실 상 전혀 관심을 두고 있지 않았다. 물론 익히 알려진 consolidation attack등을 막기 위해 몇몇 알고리즘이 바뀌었고, Windows XP SP2에는 metadata 중 security cookie를 넣기도 하였다. 또한, layout obfuscation이라고 랜덤하게 위치를 바꾸어 공격을 어렵게 하는 방법도 채택이 되었다. 그러나 이러한 방식은 복잡도, entropy가 낮아서 결국 무식한 방법, brute-force로 충분히 뚫릴 수가 있다. 8비트 짜리 security cookie가 가지는 경우의 수가 고작 몇 가지 밖에 안되기 때문이다.


그렇다면 직접 조엘이 한 말을 한 문장씩 검증해보자.

malloc이 어떻게 동작하는지 아십니까? malloc의 본질은 사용 가능한 메모리 블록을 연결 리스트 linked list로 길게 연결한 자유 체인 free chain입니다. 

사용 가능한 메모리 블록 (free node)을 관리하는 것은 맞다. 그리고 이미 사용중인 메모리 블록은 따로 관리하지 않는다. 단순히 얼마나 많은 메모리가 사용 중인가 (footprint) 정도만 기억할 뿐이고, 실제 heap에 사용 중인 메모리를 확인하려면 일일이 heap을 돌아다니면서 체크해야 한다. 그런데, 이 free node를 절대 우리가 알고 있는 linked-list 형태로 관리하는 것이 아니다. 물론 chunk struct에 forward/backward로 list 스러운 모습이 있지만 결코 linked-list와 같은 형태로 작동되지는 않는다.

malloc은 연결리스트를 따라가며, 요청 받은 메모리 양보다 큰 블록을 찾습니다.

만약 앞 문장에서 독자가 흔히 알고 있는 linked-list를 떠올렸다면 이 대목은 아주 쉽게 이해가 된다. 즉, free node들을 쭉 이은 리스트를 선형 탐색 (혹은 더 똑똑한 탐색을 하던)으로 'first-fit'으로 하는 것으로 이해할 것이다. 그러나 free node는 그냥 무식하게 다 관리가 되는 것이 아니라 그 크기 별로 매우 효율적으로 관리되고 있다. Free node는 먼저, 512 byte보다 작은 경우에는 그 크기 별로 모두 별도로 관리 된다. 물론 이 때는 linked list 형태로 관리가 된다. 즉, 16 byte 짜리 free node 리스트, 24 byte 짜리 free node 리스트, … 이런 식으로 관리한다. 이들 포인터는 smallbins에 저장이 된다. 그래서 만약 512바이트 보다 작은 메모리가 요청 된다면 이 smallbins을 뒤져서 바로 free node 하나를 때어준다. 그러니 탐색을 할 필요 없고 그냥 상수 시간에 요청이 완료 된다.

그리고 이 보다 큰 경우에는 treebins라는 Trie 자료구조를 이용해서 저장이 된다. 그래서 임의의 크기가 들어왔을 때 'best-fit'으로 적합한 free node를 찾는데 결코 시간이 오래 걸리는 작업이 아니다. 트리 같은 구조를 순회를 하기는 하지만 Trie 자료구조 특성상 depth 에 비례하는 time complexity를 가진다. 일반 binary search tree처럼 전체 노드 개수 n과 관계가 없다.

마지막으로 페이지 단위보다 큰 메모리 요청이 들어오면 그냥 바로 mmap이나 VirtualAlloc을 불러버린다. 그래서 크기를 대략 3개의 큰 분류로 나누어서 처리한다. 게다가 비교적 최신 버전 malloc 구현에서는 designated victim이라는 변수를 두어 조금이라도 순회하는 양을 줄이려고 한다.

이렇게 찾은 블록을 2개로 쪼개서, 하나는 호출한 사용자에게 반환하며, 쪼개고 난 다음에 남아있는 블록은 다시 연결 리스트에 넣어 둡니다.

정확한 표현이다. 쓰고 남은 블록은 위에서 설명했듯이 크기가 크다면 treebins으로, 크기가 작다면 바로 리스트에 연결해준다. 그리고 locality를 보존해서 캐쉬 효율성을 높이려는 부분도 엿볼 수 있다.

free를 호출할 때, free는 해제한 메모리를 자유 체인에 추가합니다.

이 말은 맞으나, 단순히 해제한 메모리를 추가하는 것 이외에 coalescing, 즉 병합이라는 과정도 병행한다. 만약 free되는 노드 앞에도 free node라면 그 자리에서 바로 backward-consolidation을 수행한다. 초창기의 heap buffer overflow 어택은 바로 이 과정을 노린 것이었다. 여기에 포인터 연산이 들어가는데, 만약 이 metadata를 엄하게 고쳐놓으면 이상한 곳으로 점프할 수 있었다. 물론 이제는 이렇게 간단하게 heap integrity가 손상 받지는 않는다. 

결국 자유 체인은 자그마한 조각으로 잘게 쪼개지므로, 큰 조각을 요청하면 원하는 크기를 충족하는 조각이 없을 수도 있습니다. 이럴 경우 malloc이 타임아웃을 선언한 다음에 자유 체인 주위를 샅샅이 훑어 조각을 정렬하고 인접한 작은 자유 블록을 더 큰 블록으로 결합합니다. 이런 작업을 하다 보면 밤이 샐 것입니다.

첫 번째 문장이야 당연히 맞는 말이지만, 타임아웃 등의 이야기는 전혀 사실이 아니다. 적어도 Doug Lea의 구현에서는 저런 부분을 찾아볼 수가 없다. 위에서도 말했듯이 인접한 작은 자유 블록을 결합하는 것은 free를 할 때 마다 수행을 이미 했다. 그래서 안정된 힙 구조에서 (즉, malloc/free 호출이 끝난 뒤), 인접한 두 블록이 모두다 자유 블록인 경우는 절대 없다. 그래서 자유 체인 주위를 샅샅이 순회해가며 삽질하는 경우가 없다. 무한정 시간이 걸리는 부분은 한 곳도 없다. 그냥 찾다 찾아도 빈 공간이 없으면 (그래 봐야 3~4단계 작업이면 바로 알 수 있음), 그냥 sbrk등을 호출해서 메모리를 늘린다.

만약, 너무 많은 메모리 요구로 힙이 매우 단편화 되어있고, 그리고 최악의 경우가 발생해 매번 자유 블록 보다 큰 메모리가 들어온다고 해도 느려지는 것은 malloc 외적인 요소, 즉 스와핑이나 과도한 페이지 폴트로 인한 것이지 malloc 자체에서 병목현상은 거의 일어나지 않는다고 볼 수 있다. 작은 크기의 malloc/free는 1초당 5백만 번 이상 일어날 수 있을 정도로 상당히 빠르다. 그러나 일반적인 프로그램에서 이런 경우는 매우 찾아보기 힘들다. malloc 때문에 밤샌다는 표현은 비록 과장이지만 너무 터무니 없고 잘못된 인상을 심어주기에 충분하다.

결국 이 모든 혼란의 끝은 malloc의 성능 특성이 결코 아주 빠르다고 볼 수 없으며(malloc은 항상 자유 체인을 돌아다닙니다), 정리하는 과정에서 종종 예측할 수 없을 정도로 지독하게 느려질 수 있다는 사실로 귀결됩니다.

따라서, 이 결론도 완전히 거짓말이다. malloc은 결코 자유 체인을 항상 돌아다니지 않는다. 돌아다니는 것이 아니라 smallbin 뒤지고, treebin 뒤지는 과정만이 있고, 이 과정은 설명하였듯이 O(n)과 같은 시간이 걸리는 것이 아니다. 예측할 수 없을 정도로 느려진다면 이미 이 프로그램 자체가 매우 메모리를 과다하게 쓴다는 그 원초적인 문제 때문이다.

물론, 예전 에서도 잠깐 언급했는데, malloc은 멀티프로세서를 고려하지 않았다. 그래서 false sharing등이 큰 문제로 대두되어 여러 방법이 나오기도 하였다. 또, 최근 심각한 문제인 heap overflow vulnerability를 어떻게 막을 것인가도 여전히 숙제이다.

주절주절 거리다 보니 정말 글이 길어졌다. 과연 여기까지 얼마나 많은 분께서 다 읽으셨는지 궁금하다. 작년 가을 malloc 가지고 사투를 벌리면서 얻은 그래프나 하나 붙여본다. 3DMark 06 이 수행 도중에 얼마나 많은 malloc을 요청하는지 데이터를 뽑아보았다. 스레드 하나가 아니라 여러 개에서 부르다 보니 이 데이터 얻는데 꼬박 하루를 날렸었다. ㅠ 보면 초당 25만 번 까지도 malloc 요청이 일어나는데 이 정도로는 전혀 heap-intensive라고 부르지 않는다. 최소 2백만 번은 불러야 좀 빡센 프로그램으로 분류할 만큼 malloc은 빠르게 돌아간다.

정작 조엘이 언급하고 있는 가비지 콜렉터 이야기는 다음에 한 번… 흔히 자바에는 가비지 콜렉터 덕 분에 memory leak이 없는 것으로 알고 있지만 이는 잘못된 사실이다. 여전히 memory leak이 존재할 수 있다.




http://egloos.zum.com/minjang/v/1232908

반응형
반응형


MSDN에서 최적화된 코딩 방법에 대한 글을 일부 번역한 것입니다. 




처리속도가 빠른 코드를 만들기 위해서는 여러 방면으로 어플리케이션을 분석하고, 그 코드가 시스템과 어떤 상관관계를 취하고 있는지 조사 할 필요가 있습니다. 여기서는 코드의 타임 크리티컬한 부분의 퍼포먼스를 향상 시키기 위한, 몇가지의 코딩 테크닉에 대해서 설명합니다. 

타임 크리티컬한 코드를 고속화 할려면 이하의 점을 확인합니다. 
• 프로그램의 어느 부분을 고속화 해야되는가 
• 코드의 사이즈와 실행속도은 어느 정도인가. 
• 새로운 기능에 관계되는 코스트는 어느 정도인가 
• 처리를 완료 할려면 최소한 어느 정도의 작업이 필요한가 

코드 퍼포먼스에 관한 정보를 수집 할 때에는 퍼포먼스 모니터(perfmon.exe)를 사용합니다. 





캐쉬 미스와 페이지 폴트 

내부 캐쉬 및 외부 캐쉬를 히트 하지 않은 경우도, 페이지 폴트(프로그램 명령이랑 데이터를 2차원 스트레이지로 검색을 하는 것)가 발생한 경우와 같이 프로그램의 퍼포먼스가 저하된다. 

CPU 캐쉬가 히트 하지 않으면 10 ~ 20 클락 분의 로스가 발생한다고 계산된다. 
외부 캐쉬가 히트 하지 않으면 20 ~ 40 클락 분의 로스가 생긴다. 페이지 폴트가 발생한 경우는 프로세스가 매초 5억의 명령어를 처리하고, 1회 페이지 폴트로 2미리초가 걸린다고 가정하면, 100만 클락 분의 로스가 생긴다. 이를테면, 프로그램을 고속화 할려면, 캐쉬의 미스 히트와 페이지 폴트를 줄이는 세세한 주의를 줄 필요가 있다. 

프로그램이 느린 원인의 하나로써, 필요 이상으로 페이지 폴트랑 캐쉬의 미스 히트가 발생하고 있다고 생각 될 수 있다. 이것을 피하려면, 데이터 참조의 국소성(局所性 )이 높은 높은 데이터 구조체를 사용해야 되므로 관련이 있는 데이터를 모아 두는 것이 중요하다. 
일견 좋게 보이는 데이터 구조체라도, 데이터 참조의 국소성이 낮기 때문에 퍼포먼스가 나빠지게 되는 경우도 있고, 역으로 너무 나쁘게 보이는 데이터 구조체라도 데이터 요소의 국소성이 높기 때문에 고속으로 되는 것도 있다. 2개의 예를 다음에 나타내면. 

동적 메모리 할당으로 만들어진 링크드 리스트는 프로그램의 퍼포먼스를 저하 시키는 원인이 된다. 리스트 내의 아이템을 참조하기도 하고, 리스트 최후까지 이동하는 경우, 링크를 되돌아 갈 때마다 캐시의 미스 히트랑 페이지 폴트가 발생 하는 경우가 있기 때문이다. 실제, 단순한 배열로 구현딘 리스트쪽이 캐시가 유효하게 동작하여 페이지 폴트 회수가 낮기 때문에, 엄청나게 고속이다. 배열의 사이즈를 확장할 때마다 여분의 처리가 필요하다는 것을 참고하여도, 역시 이쪽이 고속이다. 

동적 메모리 할당으로 만들어진 링크드 리스트를 사용한 해쉬 테이블도 퍼포먼스를 저하시키는 원인이 된다. 이와 같은 링크드 리스트에 해쉬 테이블의 내용만을 저장하고 있는 경우라도, 예상이상을 퍼포먼스가 저하하는 경우도 있다. 실제, 배열을 사용한 단순한 리니아 서치 쪽이 최종적으로는 고속이라는 예도 있다. 배열 베이스의 해쉬 테이블(일명 “클로즈 해쉬 법)은 안 좋게 보이지만 퍼포먼스적으로는 뛰어난 방법이다. 



MFC 라이브러리와 클래스 라이브러리 

MFC를 사용하면, 코드의 기술이 간단하게 됩니다. 타임 크리티컬한 코드를 기술하는 경우, 클래스에 의한 오버헤드가 생길 가능성이 있으므로 주의가 필요합니다. 타임 크맅컬한 부분에서 사용하는 MFC 코드를 조사하여 그 코드가 요구하는 퍼포먼스에 합당한지 어떨지 판단한다. MFC 클래스 및 함수에 대한 주의점을 아래에 나타냅니다. 

• CString MFC는 C 런타임 라이브러리를 호출하여 CString의 메모리를 동적으로 할당합니다. 통상, CString은 동적으로 할당돤 문자열과 같은 역할을 합니다. CString은 동적으로 할당되어 그 외의 문자열과 같이 동적 활당하여 할당 해제 때에 오버헤드가 발생합니다. 많은 경우, 스택에서 단순한 chat 형의 배열을 잡으면, 같은 처리를 고속화 할 수 있습니다. CString은 정수 문자열의 저장에 사용하지 말아야 됩니다. 정수문자열에서는 const char*를 사용합니다. CString 오브젝트를 사용하는 처리에는 보통 오버헤드가 생김으로 런 타임 라이브러의 문자열 함수를 사용한 쪽이 고속화 할 수 있는 경우가 있습니다. 

• CArray는 통상의 배열에는 없는 유연성이 있지만 프로그램에 의해 그 유연성이 필요하지 않은 경우도 있습니다. 배열의 고유의 제한을 알고 있는 경우는 사이즈 고정형의 글로벌 배열을 사용 할 수 있습니다. CArray를 사용하는 경우는 CArray::SetSize를 사용하여 사이즈와 재 할당시의 증가 요소수를 지정합니다. SetSize를 사용하지 않으면, 요소를 추가 할 때마다 배열의 재 할당과 복사를 하기 때문에 실행속도가 저하되고 메모리가 단편화 합니다. 또 배열에 아이템을 삽입하면, CArray는 메모리 내의 후속 아이템을 이동하기 때문에 배열의 학장이 필요로 하는 경우가 있습니다. 이런 처리는 캐쉬의 미스 히트와 페이지 폴트의 원인이 됩니다. MFC에서 사용하는 코드를 조사하여 퍼포먼스의 향상에 무엇이 필료한지 확인하세요. 예를 들면, CArray는 템플리트이므로 특정의 형에 대해서 CArray의 특별한 형식을 지정 할 수 있습니다. 

• CList는 더블 링크드 리스트 이므로, 리스트의 선두, 꼬리, 및 지정된 위치로의 요소 삽입이 고속입니다. 정확히 값이랑 인덱스에 의해 요소를 검색할 때에는 연속적인 검색이 필요합니다. 이것은 리스트가 길어지면 시간이 걸립니다. 코드에 더블 링크드 리스트가 필요하지 않은 경우는 CList를 사용할 필요가 있을지 어떨지 재 검토해주세요. 연속 리스트를 사용하면, 여분의 포인터 때문에 메모리를 소비 할 일이 없고, 포인터의 갱신에 의한 오버헤드가 생기는 일도 없습니다. 여분 메모리의 소비자체는 그다지 큰 문제는 없습니다만, 캐쉬의 미스 히트랑 페이지 디폴트를 증가시키는 원인이 됩니다. 

• IsKindOf 이 함수는, 많은 함수 호출을 하는 것에 의해, 다른 데이터 영역에 다양한 메모리 접근을 합니다. 다시 말해, 데이터 참조가 국소적이지 않습니다. 이 함수는 디버그 빌드에서는 편리하여 ASSERT 호출등에서 사용됩니다. 릴리즈 빌드에서는 사용은 피해 주십시오 




출처 : http://coder.egloos.com/m/2804749


반응형
반응형

http://yesarang.tistory.com/214


오랜만에 S/W개발 관련된 글을 올립니다. 요즘 블로그에 제대로 글을 쓰지도 못하는데 계속해서 RSS 구독자 카운트는 늘어만가니 어찌된 조화인지 모르겠습니다. 늘어나는 구독자수의 압박을 견디다 못해 오래전에 약속드렸던 reentrant와 thread-safe의 차이점에 대해 말씀드릴까 합니다.

우선 reentrance 를 우리말로 옮기자면 재진입성쯤 되고, thread-safety는 multithread-safety라고도 하니 다중쓰레드안전성쯤 되겠습니다. 요즘 워낙 multi-threaded programming은 일반화된 것이라 thread-safety에 대해서는 어느 정도 개념들을 챙기고 계시리라 생각하고, reentrance가 thread-safety와 어떻게 다른가를 설명하면서 그 둘간의 차이점을 설명해 보도록 하겠습니다.

어떤 루틴 또는 프로그램이 Thread-safe하다라고 하면 여러 쓰레드에 의해 코드가 실행되더라도 실행 결과의 correctness가 보장되는 것을 뜻합니다. 이 정의에서 중요한 점은 실행 결과의 correctness가 보장되어야 한다는 것입니다. 즉, 여러 쓰레드에 의해 코드가 진짜로 동시에 수행되던 보이기에 동시에 수행되던 간에 실행 결과의 correctness가 보장되면 Thread-safe하다라고 말하는 거죠.

이에 비해 어떤 루틴 또는 프로그램이 Reentrant하다라고 하면 여러 쓰레드에 의해 코드가 동시에 수행될 수 있고, 그런 경우에도 실행 결과의 correctness가 보장되는 것을 뜻합니다.

이해가 되시나요 ? 차이점이 안 보이신다구요 ? 그럼 다시 한 번 말씀드리죠.

어떤 루틴 또는 프로그램이 Thread-safe하다라고 하면 여러 쓰레드에 의해 코드가 실행되더라도 실행 결과의 correctness가 보장되는 것을 뜻합니다.

이에 비해 어떤 루틴 또는 프로그램이 Reentrant하다라고 하면 여러 쓰레드가 코드를 동시에 수행할 수 있고, 그런 경우에도 실행 결과의 correctness가 보장되는 것을 뜻합니다.

이제 보이시죠 ? Reentrant 특성이 훨씬 강력한 제약조건이라는 것이 느껴지시나요 ? 구체적으로 코드에서는 어떻게 달라지나 예제를 통해서 알아보겠습니다.

C 언어 표준라이브러리에 strtok()이라는 문자열 함수가 있는 건 다 알고 계시겠죠(모르시는 분은 가서 엄마 젖 좀 더 먹고 오세요~ ㅋㅋㅋ 농담입니다). strtok() 의 매뉴얼에 다음과 같이 설명되어 있습니다.

     The strtok() function can be used to break the string
     pointed to by s1 into a sequence of tokens, each of which is
     delimited by one or more characters from the string pointed
     to by s2. The strtok() function considers the string s1 to
     consist of a sequence of zero or more text tokens separated
     by spans of one or more characters from the separator string
     s2. The first call (with pointer s1 specified) returns a
     pointer to the first character of the first token, and will
     have written a null character into s1 immediately following
     the returned token. 
The function keeps track of its position
     in the string between separate calls, so that subsequent
     calls (which must be made with the first argument being a
     null pointer) will work through the string s1 immediately
     following that token.
 In this way subsequent calls will work
     through the string s1 until no tokens remain. The separator
     string s2 may be different from call to call. When no token
     remains in s1, a null pointer is returned.


strtok()을 사용하는 예제를 보면 그 의미가 더 명확해집니다.

#include <cstring>

using namespace std;

int
main()
{
  // strtok() 의 첫번째 argument 는 const char * 가 아닌 char * 이므로
  // 새로 할당된 메모리로 넣음
  char* str = (char*)malloc(strlen("A;B;C;D;E;F;G")+1);
  strcpy(str, "A;B;C;D;E;F;G");
  cout << str << " ==> ";

  char* tok = strtok(str, ";");
  while (tok != 0)
  {
    cout << tok << " ";
    tok = strtok(0, ";");
  }
  cout << endl;

  return 0;
}


위와 같이 작동한다는 것은 strtok() 내부적으로 다음과 같이 다음번 호출 때 문자열 분석을 시작할 위치를 기억하고 있다는 걸 뜻합니다. 그리고 여러번의 호출에 걸쳐 이 함수가 제대로 작동하기 위해서는 그 위치는 당연히 static 으로 선언되어 있겠지요.

char* strtok(char* src, const char* delim)
{
  // src, delim 이 NULL 인지, delim 이 "" 인지 체크하는 코드는 생략
  char* tok;
  static char* next;     // 분석을 시작할 위치
  if (src != NULL)
    next = src;
  tok = next;

  // boundary condition check
  if (*next == '\0')
    return NULL;

  // 분석 시작
  for (; *next != '\0'; ++next)
  {
    if (*next in delim) // pseudo code
    {
      *next = '\0';
      ++next;
      break;
    }
  }

  return tok;
}

위와 같은 코드는 Reentrant 하지도 않고 Thread-Safe 하지도 않을 것입니다. 위 코드가 여러 쓰레드에 의해 수행될 경우, next 라는 변수가 서로 다른 쓰레드에 위해 공유가 되므로(함수 내에서 static으로 선언된 변수는 stack에 할당되는 것이 아니라 전역 메모리 영역에 할당됩니다) next 변수가 깨질 가능성이 생기기 때문입니다. 이런 상황에 대한 구체적인 예를 다음과 같이 들 수 있을 것 같습니다.



Multi-thread 상의 strtok() 호출

위 그림에서 Call strtok() 앞의 숫자는 호출 순서를 뜻하는 것이고 next가 가리키는 화살표의 선은 Call strtok()을 호출한 후 next 변수의 상태값이라고 생각하시면 됩니다. 그림의 예에서는 3, 4 번까지는 원래 예상하는 결과 대로 리턴이 되겠지만 5번 호출에서는 "F"가 리턴될 것이고, 6번 호출에서는 NULL이 리턴될 것입니다(왜 그런지는 제가 작성한 strtok()을 따라가면서 알아보세요. ^^). 이런 결과가 나오는 것은 모두 next 라는 변수가 양쪽 Thread에 의해서 값이 갱신되기 때문입니다.

그렇다면 strtok()을 thread-safe 하게 만들기 위해서는 어떻게 해야할까요 ? 다음 처럼 strtok() 내부 구현을 바꿔서 내부적으로 next 변수에 대해 lock을 걸도록 만들면 될까요 ?

os_specific_lock lck;

char* strtok(char* src, const char* delim)
{
  // src, delim 이 NULL 인지, delim 이 "" 인지 체크하는 코드는 생략
  char* tok;
  lock(&lck);
  static char* next;     // 분석을 시작할 위치
  if (src != NULL)
    next = src;
  tok = next;

  // boundary condition check
  if (*next == '\0')
  {
    unlock(&lck);
    return NULL;
  }

  // 분석 시작
  ......

  unlock(&lck);
  return tok;
}


언뜻 생각하면 위와 같이 구현된 strtok()이 thread-safe하다고 생각할 수도 있겠지만 실상은 전혀 그렇지 않습니다. 위와 같이 구현된 strtok()은 thread 가 동시에 next 변수를 수정하는 것을 막아주기 하지만 그림에서 나타낸 예를 제대로 처리해주지 못합니다. strtok()이 thread-safe 하기 위해서는 strtok() 호출 한번에 대해 thread-safe하면 되는 것이 아니라 전체 tokenizing 과정이 모두 thread-safe 해야 하기 때문입니다. 아~ 그렇군요. 그렇다면 tokenizing 과정 전체에 대해 lock을 걸면 되겠네요.

#include <cstring>

using namespace std;

os_specific_lock lck;

void* tokenizer(void* s)
{
  char* str = (char*)s;
  cout << str << " ==> ";

  lock(&lck);
  char* tok = strtok(str, ";");
  while (tok != 0)
  {
    cout << tok << " ";
    tok = strtok(0, ";");
  }
  unlock(&lck);
  cout << endl;

  return 0;
}

int
main()
{
  pthread_t thr1, thr2;
  char* str1, str2;      // 어찌어찌해서 초기화됐다고 가정
  pthread_create(&thr1, NULL, tokenizer, str1);
  pthread_create(&thr2, NULL, tokenizer, str2);

  return 0;
}

어때요? 맘에 드시나요 ? tokenizing 과정 전체에 대해 lock을 걸어 버렸으니 제가 그림에서 제시한 상황이 발생하지 않겠네요. 그래도 어쩐지 꺼림직하지 않으세요 ? 마치 구더기 한 마리 잡으려고 불도저 쓰는 격이라고나 할까요. 위에 있는 tokenizer는 아주 간단해서 그렇지 만약에 token 하나 하나에 대해서 복잡한 처리를 한다면 어떻게 될까요 ? 그리고 복잡한 처리 과정 중에 그 쓰레드가 I/O 이벤트를 기다린다면 어떻게 될까요 ? 갈수록 태산이네요. 그죠 ? CPU가 아무리 빨라도 아무리 많은 Multi Core 들을 가지고 있어도 전혀 그런 성능을 활용하지 못하는 코드가 되어 버립니다. 이 사태를 어떻게 해결해야 하나요. 여러분이 진정한 엔지니어라면 여기서 멈춰서는 안돼죠. 해결책을 생각할 시간을 드리겠습니다.
1초.
2초.
3초.
4초.
5초.
6초.
7초.
8초.
9초.
10초 삐~~~~~!!!.

생각나셨나요 ? 멋진 해결책을 가지고 계신 분들이 있으리라 생각합니다. 자~ 그럼 다음과 같이 strtok() 인터페이스 및 구현을 바꾸면 어떨까요 ?

char* strtok(char* src, const char* delim, char** start)
{
  // src, delim 이 NULL 인지, delim 이 "" 인지 체크하는 코드는 생략
  char* tok;
  char* next;     // 분석을 시작할 위치. static 을 없애고 start 라는 입력
                  // 으로 초기화함
  if (src != NULL)
    next = src;
  else
    next = *start;


  // boundary condition check
  if (*next == '\0')
    return NULL;

  // 분석 시작
  tok = next;
  for (; *next != '\0'; ++next)
  {
    if (*next in delim) // pseudo code
    {
      *next = '\0';
      ++next;
      break;
    }
  }

  *start = next;
  return tok;
}


이렇게 구현할 경우 thread-safe 하기도 하지만 reentrant하기도 합니다. 위와 같은 strtok() 내에서 사용되는 모든 변수는 stack에 할당되는 자동변수이므로 각각 독립적인 stack을 갖는 thread가 동시에 위와 같은 strtok()을 수행한다해도 전혀 문제가 없습니다. 실상 위와 같이 구현한 strtok()은 POSIX에 의해 표준화되어 있는 strtok_r()이나 MS Visual C++ 에서 제안한 safe string library에 표함되어 있는strtok_s()와 동일합니다.

reentrant 한 코드는 thread 간에 공유하는 자원 자체가 없어야만 하는 코드입니다. 쓰레드간에 동기화 메커니즘 자체가 필요 없게 만드는 코드이고, 따라서 multi threading 환경에서 여러 쓰레드가 해당 코드를 진짜로 동시에 실행하더라도-동시에 실행되는 것처럼 보이기만 하는 것이 아니라-아무런 문제가 없습니다. 그렇지만 thread-safe 하다는 것은 단지 여러 쓰레드에 의해 실행되더라도 문제만 없으면 된다는 완화된 조건이므로 공유하는 자원이 있더라도 이것을 여러 쓰레드가 동시에 접근하지 못하도록 locking mechanism 같은 것으로 막아주기만 하면 됩니다. 결국 thread-safe한 코드는 multi threading 환경에서 reentrant 코드보다는 효율성이 떨어질 가능성이 높습니다. 해당 코드를 수행하고 있는 thread 가 공유 자원에 대한 lock 이 풀리기를 기다리는 동안은 다른 thread 의 수행을 막아버리기 때문입니다.

또 다른 간단한 예를 통해 thread-safe와 reentrant 의 차이점을 살펴 보겠습니다.

// 출처: Wikipedia
int g_var = 1;

int f()
{
  g_var = g_var + 2;
  return g_var;
}

int g()
{
  return f() + 2;
}

위와 같은 코드에서 g() 또는 f()를 호출하는 코드는 모두 thread-safe 하지 않습니다. thread-safe 하지 않으면 reentrant 하지도 않습니다. 위 코드를 thread-safe 하게 하려면 어떻게 해야할까요 ? 다음과 같이 해야겠죠.

int g_var = 1;
os_specific_lock lck;

int f()
{
  lock(&lck);
  g_var = g_var + 2;
  unlock(&lck);
  return g_var;
}

int g()
{
  return f() + 2;
}

이제 thread-safe하게 됐습니다. reentrant 할까요 ? 아니올시다입니다. 여러 쓰레드가 f() 함수를 동시에 수행할 수 없기 때문입니다. 다시 말하면 한 쓰레드가 lock 을 걸고 있다면 다른 쓰레드는 lock 이 풀릴 때까지 기다려야 하므로 reentrant 하지 않은 것입니다. 위 코드를 reentrant하게 고치려면 어떻게 하면 될까요 ?

// 출처: Wikipedia
int f(int i) 

  int priv = i;
  priv = priv + 2;
  return priv;
}

int g(int i)
{
  int priv = i;
  return f(priv) + 2;
}

아예 전역 변수를 없애 버려서 쓰레드간 동기화가 필요 없게 만들어 버렸습니다. 위 코드에서는 워낙 lock이 걸리는 시간이 적을 것이므로 성능에 거의 영향을 미치지 않겠지만, lock이 걸리는 기간이 길어진다면 성능에 상당한 영향을 미치겠지요. 특히 요즘 유행하는 Multi-Core CPU에서는 그냥 thread-safe 코드와 reentrant 코드간의 성능 차이가 더 많이 발생할 것입니다.

보통은 thread-safe 코드를 만드는 것보다는 reentrant한 코드를 만드는 게 더 어렵습니다. 그리고, thread-safe 한 코드는 내부 구현만 바꾸면 되는 경우가 많지만 reentrant한 코드를 만드는 것은 위에서 제가 제시한 두 가지 예(strtok(), f())처럼 아예 인터페이스 자체를 재설계해야 하는 경우가 많습니다.

가능하다면 그냥 thread-safe한 코드를 만드는 것보다는 reentrant한 코드를 만드는 것이 성능상 훨씬 좋은 선택입니다. 물론 reentrant 한 코드를 만드는 것이 불가능한 경우도 있겠지만, 고민해보면 reentrant한 코드를 만들 수 있는 경우가 상당히 있습니다. 요즘처럼 Multi-Core CPU가 갈수록 일반화되고 있는 상황에서 Reentrance는 다시 한 번 주목을 받아야 할 것입니다.

그럼 Coding Guideline스러운 멘트로 이번 글을 마무리 하도록 하겠습니다. ^^

"Thread-safe한 코드보다는 Reentrant한 코드로 작성하라"

반응형
반응형

http://cafe.naver.com/ongameserver/3952



근래에 회사에서 만들고 있는 게임의 서버 애플리케이션에 LFH을 적용하면서 관련 자료를 좀 모아 보았습니다.


LFH ( Low fragmentation Heap )

1. 단편화 방지를 위해서 만들어진 것이지만 멀티코어 환경에서 특히 좋다.

2. 코어가 하나 있을 때는 오히려 10% 성능 하락도 있다( 신뢰 여부는 측정 불가 ^^; )

3. CRT에서는 크게 성능 향상을 볼 수 없다.
   (nhn Japan 블로그 글에서 CRT도 LFH를 적용했는지 정확하게 모르겠음
    CRT도 LFH 적용을 위해서는 CRT 힙 핸들을 HeapSetInformation에 적용해야 한다 )

4. LFH가 극상의 성능을 내기 위해서 Private Heap과의 조합이 최상이다.

5. Windows Server 2008/Vista 기본적으로 사용되도록 되어 있음
   : 이 때문에 메모리 측정(Private Bytes)를 할 때 정보가 좀 틀릴 수도 있음

6. LFH는 Windows XP, Windows 2000 Professional with SP4, Windows Server 2003 이상 가능



사용 방법

1. 프로세스 힙에 LFH 적용
ULONG ulEnableLFH = 2;
HeapSetInformation (
        GetProcessHeap(), 
        HeapCompatibilityInformation, 
        &ulEnableLFH, 
        sizeof(ulEnableLFH));

2. CRT 힙에 LFH 적용
intptr_t hCrtHeap = _get_heap_handle();
ULONG ulEnableLFH = 2;
if (HeapSetInformation((PVOID)hCrtHeap,
                           HeapCompatibilityInformation,
                           &ulEnableLFH, sizeof(ulEnableLFH)))
        puts("Enabling Low Fragmentation Heap succeeded");
    else
        puts("Enabling Low Fragmentation Heap failed");
    return 0;


반응형
반응형

http://valley.egloos.com/viewer/?url=http://byung.egloos.com/4812368


골치 아픈 Memory Fragmentation

byung.egloos.com/4812368

Application Debugging을 하다 보면간간히 들어오는 Issue OOM(Out Of Memory)에 대한 이슈인데이거 생각보다 골치가아프다사실 이를 위해서 DebugDiag  UMDH 라는 훌륭한 Tool에 의지하며, Allocation pattern을 Check 하는 것은debugging도 아니지만눈에 띄는 Allocation pattern이나 메모리상에 보유하는 있는 실제 Allocation(Committed) region이 그다지 높지 않은 데도 불구하고 OOM현상이 발생할 수 있다는 데에 어이없어 질 때가 있다그러면서 Application 개발자는 분명히Allocation/release 를 엄격하게 지켰을 뿐인데... 라고 말하기도 한다이런 경우 무엇을 할 수 있을 까.

이는 Memory Fragmentation 현상으로 판단한다상당히 자잘한 Memory Alloc이 셀 수 없이 반복되면서 메모리 조각화가 발생하는 것이다간혹큰 메모리 덩어리가 할당/해제 되기도 하고 그러면서 메모리 조각화는 더 할당 받을 수 있는 free 영역이 있음에도 불구하고 !address Check해보면연속된 여분의 메모리 블럭이 존재하지 않아서 OOM을 유발하게끔 한다이러한 경우에 일차적으로 할 수 있는 것은 1) 어찌됐든 불필요한 작고 많은 memory 조각들이 Leak으로 존재하는 부분을 제거해야만 한다.이는 DebugDiag Leak tracking을 통해서 Checking될 수 있다. DebugDiag에서 제공하는 Leak 분석에서 제공하는 Callstack 정보는 그 메모리 보유 Size가 크지 않더라도 Allocation Count Check하여 메모리상에 산재됨으로 인한 Fragmentation의 원인이 되진 않는 지 Check 해볼 수 있다.

2) 두 번째는 문서 http://support.microsoft.com/kb/315407 에서 제공하는 registry key, HeapDecommitFreeBlockThreshold 가 도움이 될 수 있다이는 레지스트리에 설정된 값 이상이 되는 경우가 아니면재사용을 위해 decommitted 되지 않는 상태로 유지하는 것이다.

 

HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager

HeapDecommitFreeBlockThreshold REG_DWORD - 0x00040000 (권고)

 

3)사실 2번째 방법이 Application을 위해서 얼마나 효율적인지 경험해보진 못했다아마도 Windows XP/2003에서 도입된 LFH (Low Fragmentation Heap)을 사용하도록 Application을 구성하는 것이 가장 효율적일지 모르겠다이는 문서http://msdn.microsoft.com/en-us/library/aa366750.aspx 에도 언급되었지만, HeapSetInformation API를 이용해서HeapCompatibilityInformation(0) 값을 parameter 전달함으로써 Application Level에서 설정할 수 있다. http://msdn.microsoft.com/en-us/library/aa366705(VS.85).aspx 문서에는 다음과 같은 예제를 확인할 수 있다이는 Application에서 사용하는 모든 Heap을 LFH를 사용하도록 설정할 수 있다.

 

HANDLE heaps[1025];

  DWORD nheaps = GetProcessHeaps(1024, heaps);

 

  for (DWORD i = 0; i < nheaps; i++) {

    ULONG  HeapFragValue = 2;

    HeapSetInformation(heaps[i],

                       HeapCompatibilityInformation,

                       &HeapFragValue,

                       sizeof(HeapFragValue));

  }

이러한 것들이 Memory Fragmentation을 위해 도움이 될 듯 하다.


반응형
반응형
윈도우 힙 

  : 윈도우 힙은 모든 윈도우 응용프로그램이 공유하는 큰 메모리 공간으로, 주로 큰 메모리 블록을 할당 받고자
할때 사용합니다. 


CRT힙

  :  윈도우 힙과는 별도로, 프로그램마다 독립적으로 존재하는 힙. 각각의 프로세스는 윈도우 힙의 일부를
할당 받아 CRT힙으로 사용합니다.  CRT힙은 적은 양의 메모리 할당 요청에 대해 최적화 되어서 적은양의
메모리가 필요할때 CRT힙을 사용하는 것이 좋다.

C/C++ 언어의 표준 라이브러리 함수인 malloc 함수나  new 연산자를 사용한다.

초기에 64kb의 메모리 공간을 차지하며, 이 공간을 다 쓰게되면 메모리 공간을 두배씩 늘려갑니다.

동작은 malloc 보다 new 연산자가 빠르게 동작한다.
 


반응형
반응형

http://cafe.naver.com/jzsdn/17321


===== 19. 메모리 풀 =====
슈팅게임에서는 많은 오브젝트가 생성되고, 삭제되므로 
빠른 힙메모리 생성및 삭제가 필요하다.
그래서, 여기서는 MS가 만든 메모리 풀을 사용한다.


1. LFH사용설정
2. LFH호출
3. Low-Fragmentation Heap의 장점
4. Low-Fragmentation Heap사용시 참고

 

 


1. LFH사용설정
BOOL CheckLFH(VOID)
{
        ULONG  HeapFragValue = 2;

        if(HeapSetInformation(GetProcessHeap(),
                HeapCompatibilityInformation,
                &HeapFragValue,
                sizeof(HeapFragValue))
                )
        {
                return TRUE;
        }
        return FALSE;
}

프로세스의 힙핸들을 가져와서 설정하는것만으로 
메모리 풀을 사용하는 모든 절차는 끝이난다.

 

 

 


2. LFH호출
BOOL bLFH = CheckLFH();

if (bLFH)
        MessageBoxW( NULL, L"LFH를 사용합니다", L"정보", MB_OK );

프로그램이 시작할때 한번호출해준다.
디버거가 연결되면(F5) 디버그 힙을 사용하므로 CTRL+F5를 눌러야 
Low-Fragmentation Heap이 사용되고, TRUE가 리턴되는것을 알수있다.

 

 


3. Low-Fragmentation Heap의 장점
특별히 프로그래머는 신경쓰지 않아도, 메모리풀을 사용,
프로그램의 질을 높일수있다.
메모리 단편화를 예방할수있고, 힙메모리생성, 삭제시 효율이 높아진다.
특히, 멀티코어 시스템에서 높은 성능향상이 있다.

 

 

 


4. Low-Fragmentation Heap사용시 참고
이 함수가 호출되지 않아도, 이코드가 있으면 Win2000Pro Sp4이상의 
OS에서만 동작하므로, Win98을 지원하려고 한다면, 주석화시켜야 한다.

비스타는 이함수를 호출하지 않아도 기본동작한다.
이 함수는 하나의 프로세스에 메모리풀을 사용함으로 멀티프로세스로 동작하려면, 수정이필요하다.
16K보다 작은 메모리를 할당하면, LFH에 메모리가 할당, 그이상은 기존힙에서 할당된다.



  • 2009/07/28 10:30

    잘 봤습니다.
    음.. 궁금한 점이 있는데 new/delete도 HeapAlloc(GetProcessHeap()....) 으로 잡고
    HeapFree(GetProcessHeap(), ...)로 delete시키도록 오버로딩 시켜야 되지 않을까요??

  • 2009/07/28 21:01

    new/delete, malloc/free가 내부적으로 HeapAlloc/HeapFree를 호출합니다.


반응형
반응형

http://blog.naver.com/silver6688/60047045316



VOID GlobalMemoryStatus(

  LPMEMORYSTATUS lpBuffer   // pointer to the memory status structure

);

MEMORYSTATUS의 구조체를 보면


typedef struct _MEMORYSTATUS { 

    DWORD dwLength;                                     //구조체 크기

    DWORD dwMemoryLoad;                           //메모리 사용률

    SIZE_T dwTotalPhys;                                //전체 메모리

    SIZE_T dwAvailPhys;                                //사용한 메모리

    SIZE_T dwTotalPageFile; 

    SIZE_T dwAvailPageFile; 

    SIZE_T dwTotalVirtual;                              //총가상메모리

    SIZE_T dwAvailVirtual;                              //사용한 가상 메모리

} MEMORYSTATUS, *LPMEMORYSTATUS; 


라고 되어 있습니다.

MEMORYSTATUS ms;

GlobalMemoryStatus(&ms); 

ms.dwTotalPhys - ms.dwAvailPhys  

 

ex)

MEMORYSTATUSEX memStatus;

 memset( &memStatus, 0, sizeof( MEMORYSTATUSEX ) );

 memStatus.dwLength = sizeof( MEMORYSTATUSEX );

 

 GlobalMemoryStatusEx( &memStatus );

 char szPhysicalMemory[MAX_PATH] = _T("");

 sprintf( szPhysicalMemory, "메모리 용량 : %ld Mbytes", memStatus.ullTotalPhys / MEMORY_DIVIDER / MEMORY_DIVIDER );

 m_ctrSystemTree.SetInsertItem( hParent, szPhysicalMemory );

 char szVirtualMemory[MAX_PATH] = _T("");

 sprintf( szVirtualMemory, "가상 메모리 : %ld Mbytes", memStatus.ullTotalVirtual / MEMORY_DIVIDER / MEMORY_DIVIDER );

 m_ctrSystemTree.SetInsertItem( hParent, szVirtualMemory );

 char szPagingMemory[MAX_PATH] = _T("");

 sprintf( szPagingMemory, "페이징 파일 최대크기 : %ud", memStatus.ullTotalPageFile );

 m_ctrSystemTree.SetInsertItem( hParent, szPagingMemory );


반응형
반응형


http://goo.gl/u0UL1o


작업 관리자 메모리 열의 의미

작업 관리자에서 프로세스 탭에 표시되는 정보에 열을 추가하면 컴퓨터에서 실행 중인 프로세스를 모니터링할 수 있습니다. 이러한 열에는 프로세스의 현재 CPU(Central Processing Unit) 및 메모리 리소스 사용량 등 각 프로세스에 대한 정보가 표시됩니다.

  1. 작업 표시줄을 마우스 오른쪽 단추로 클릭한 다음 작업 관리자를 클릭하여 작업 관리자를 엽니다.

  2. 프로세스 탭을 클릭합니다. 작업 관리자에서 해당 사용자 계정으로 현재 실행 중인 프로세스를 표시합니다. 모든 사용자에 대해 실행 중인 프로세스를 표시하려면 모든 사용자의 프로세스 표시를 클릭합니다. 관리자 권한 필요 관리자 암호나 확인을 묻는 메시지가 표시되면 암호를 입력하거나 확인을 제공합니다.

  3. 기타 열을 추가하려면 보기를 클릭한 다음 열 선택을 클릭합니다. 표시할 열의 확인란을 선택한 다음 확인을 클릭합니다.

설명

PID(프로세스 식별자)

실행되는 동안 프로세스를 고유하게 식별하는 번호입니다.

사용자 이름

프로세스를 실행 중인 사용자 계정입니다.

세션 ID

프로세스의 소유자를 식별하는 번호입니다. 여러 명의 사용자가 로그온한 경우 각 사용자에 고유한 세션 ID가 있습니다.

CPU 사용

마지막 업데이트 이후 프로세스에서 CPU(Central Processing Unit)를 사용한 시간의 백분율이며 열 제목이CPU로 표시됩니다.

CPU 시간

시작된 이후 프로세스에서 사용한 총 프로세서 시간(초)입니다.

메모리 - 작업 집합

개인 작업 집합의 메모리 양과 프로세스에서 사용하고 있으며 다른 프로세스와 공유할 수 있는 메모리 사용량을 더한 것입니다.

메모리 - 최고 작업 집합

프로세스에서 사용하는 작업 집합 메모리 중 최대 메모리 양입니다.

메모리 - 작업 집합 변화량

프로세스에서 사용한 작업 집합 메모리의 변화량입니다.

메모리 - 개인 작업 집합

특히 프로세스에서 사용 중이며 다른 프로세스와 공유할 수 없는 메모리 양을 설명하는 작업 집합의 하위 집합입니다.

메모리 - 커밋 크기

프로세스에서 사용하려고 예약한 가상 메모리의 양입니다.

메모리 - 페이징 풀

하드 디스크 등의 다른 저장 미디어에 기록할 수 있는, 프로세스에 대해 커밋된 가상 메모리 양입니다.

메모리 - 비페이징 풀

다른 저장 미디어에 기록할 수 없는, 프로세스에 대해 커밋된 가상 메모리 양입니다.

페이지 폴트

메모리에 없으므로 디스크에서 프로세스의 데이터를 검색해야 하는 횟수입니다. 페이지 폴트 값은 프로세스가 시작된 시간부터 누적됩니다.

페이지 폴트 변화량

마지막 업데이트 이후 페이지 폴트 수의 변화입니다.

기본 우선 순위

프로세스의 스레드가 예약되는 순서를 결정하는 우선 순위입니다.

핸들

프로세스의 개체 표에 있는 개체 핸들 수입니다.

스레드

프로세스에서 실행 중인 스레드 수입니다.

USER 개체

현재 프로세스에서 사용 중인 USER 개체 수입니다. USER 개체는 창 관리자의 개체로, 창, 메뉴, 커서, 아이콘, 후크, 가속기, 모니터, 자판 배열 및 기타 내부 개체가 포함됩니다.

GDI 개체

그래픽 출력 장치에 대한 API(응용 프로그래밍 인터페이스)의 GDI(그래픽 장치 인터페이스) 개체 수입니다.

I/O 읽기

파일, 네트워크 및 장치 I/O를 포함하여 프로세스에서 생성한 입출력 읽기 작업 수입니다. CONSOLE(콘솔 입력 개체) 핸들로 보낸 I/O 읽기는 계산되지 않습니다.

I/O 쓰기

파일, 네트워크 및 장치 I/O를 포함하여 프로세스에서 생성한 입출력 쓰기 작업 수입니다. CONSOLE(콘솔 입력 개체) 핸들로 보낸 I/O 쓰기는 계산되지 않습니다.

I/O 기타

파일, 네트워크 및 장치 I/O를 포함하여 읽기 및 쓰기가 아닌 프로세스에서 생성한 입출력 작업 수입니다. 이러한 작업 유형의 예로 제어 기능이 있습니다. CONSOLE(콘솔 입력 개체) 핸들로 보낸 I/O 기타 작업은 계산되지 않습니다.

I/O 읽기 바이트

파일, 네트워크 및 장치 I/O를 포함하여 프로세스에서 생성한 입출력 작업에서 읽은 바이트 수입니다. CONSOLE(콘솔 입력 개체) 핸들로 보낸 I/O 읽기 바이트는 계산되지 않습니다.

I/O 쓰기 바이트

파일, 네트워크 및 장치 I/O를 포함하여 프로세스에서 생성한 입출력 작업에서 쓴 바이트 수입니다. CONSOLE(콘솔 입력 개체) 핸들로 보낸 I/O 쓰기 바이트는 계산되지 않습니다.

I/O 기타 바이트

파일, 네트워크 및 장치 I/O를 포함하여 읽기 및 쓰기가 아닌 프로세스에서 생성한 입출력 작업에서 전송된 바이트 수입니다. 이러한 작업 유형의 예로 제어 기능이 있습니다. CONSOLE(콘솔 입력 개체) 핸들로 보낸 I/O 기타 바이트는 계산되지 않습니다.

이미지 경로 이름

하드 디스크에 있는 프로세스의 위치입니다.

명령줄

프로세스를 만들기 위해 지정된 전체 명령줄입니다.

가상화

UAC(사용자 계정 컨트롤) 가상화가 사용되는지 여부 또는 이 프로세스에 대해 허용되지 않는지 식별합니다. UAC 가상화는 파일 및 레지스트리 쓰기 오류를 사용자별 위치로 리디렉션합니다.

설명

프로세스에 대한 설명입니다.

데이터 실행 방지

이 프로세스에 대해 데이터 실행 방지가 사용되는지 여부입니다. 자세한 내용은 데이터 실행 방지란?을 참조하십시오.


반응형
반응형
http://goo.gl/YsesDP




우선 dll 을 만들어 컴파일 하면, 프로젝트명과 동일한 헤더파일, lib 파일, dll 파일이 

생성이 생성되는데...


묵시적으로 dll 을 로드할 경우 위 3개의 파일이 모두 필요 합니다.
(헤더파일이 없어도 되지만, 왜? 헤더까지 필요한지는 아래에 추가로 언급)

dll 의 헤더파일 : 컴파일 할 때 dll 에 있는 함수의 원형을 알아야 한다.
                        -> dll 의 헤더파일이 필요한 이유는 조금 아래에 설명~
dll 의 lib 파일: 링크 시킬 때, dll 에 있는 함수의 몸체(정의부) 를 연결 시켜 실행파일 생성시 필요
                     -> 프로젝트 속성 또는 #pragma comment(lib,,,,) 에서 lib 링크 처리 필요~
dll 파일 : exe 파일이 실행 될 때, dll 에 있는 함수를 호출할 때 필요

->  이 중, 헤더파일, lib 파일은 사용하려는 곳의 소스파일이 있는 폴더에 복사를 시켜두고,
      dll 파일은 exe 실행파일이 있는 곳에 복사를 해 둡니다.


아마. dll 의 헤더파일이 왜?? 필요한가?? 궁금할 텐데...질문한 코드에 보면 묵시적으로 dll 의 함수를
로드할 때,,,

extern "C" __declspec(dllimport) int my_add(int a,int b); 라는 부분이 보입니다.

만약, dll 에서 로드할 함수의 수가 굉장히 많다면, 저 함수들을 사용하는 곳에서 저렇게 일일이 함수 원형 선언을 해주는 것은 노가다 행동일 뿐이죠~

즉, dll 의 헤더파일을 사용하려는 곳에도 복사해주는 이유는 이런 불편함을 줄이기 위함 입니다.

고로, 헤더파일과 cpp 파일을 다음과 같이 수정해 줍니다.

- my_Math.h -

#pragma once

#ifdef DLLEXPORT  // DLLEXPORT 상수가 선언되어 있다면...
#define DLLTYPE __declspec(dllexport)
#else  // 그렇지 않다면...
#define DLLTYPE __declspec(dllimport)
#endif 

extern "C"  DLLTYPE int my_add (int a, int b);

...이하 동일


- my_Math.cpp -


#define  DLLEXPORT  // DLLEXPORT 상수선언 (export  될 것임을 명시하는 부분)

#include "my_Math.h"


extern "C"  DLLTYPE 
int my_add (int a, int b)
{
 .......
}

...이하 동일...



명시적으로 로드할 경우는 위 3개 파일 중에서 dll 파일 하나만 있으면 됩니다.
이 경우, dll 파일 역시, exe 파일이 있는 곳의 폴더에 복사를 해두면 되겠습니다.

일단,,,dll 프로젝트를 빌드하여 lib 파일 dll 파일이 제대로 생성되는지를 본인이 확인해야 합니다.

__declspec(...)  <-- 이 문법에서도 언더바 __ 가 2개 인 것도 확인 하시고...

dll 프로젝트를 빌드하여 제대로 lib, h파일, dll 파일이 생성되었으면 일단 dll 을 사용할 준비가
된 겁니다.

이 생성된 파일들을 위에서 언급한 곳으로 각각 이동을 시킵니다.


- 명시적 실행 소스 코드 -


#include <stdio.h>
#include <windows.h>


// 명시적으로 로드하는 경우, dll 에서 직접 함수의 주소를 읽어 오므로

// 함수의 원형선언 조차도 필요 없습니다.

//_declspec(dllimport) int my_sub(int,int);
//_declspec(dllimport) int my_add(int,int);


typedef int (*ALUFunc)(int,int);


int main()
{
   int a=3,b=5;
   int result;

 ALUFunc my_func ;  //함수 포인터 선언

 HINSTANCE hInstLib = LoadLibrary("___.dll");  // dll 파일명 맞는지 확인할 것 !!

 if (hInstLib == NULL)
{
     printf("오류: DLL을 불러올 수 없습니다\n");
                return 1;
 }
    my_func = (ALUFunc)GetProcAddress(hInstLib,"my_add");

   if (my_func==NULL)
   {
      printf("오류: DLL 함수를 찾을 수 없습니다\n");
                FreeLibrary(hInstLib);
                return 1;

   }

        // 함수 요청하기.
        result = my_func(1, 2);
 
        // DLL 파일의 로드를 해제한다
        FreeLibrary(hInstLib);
 
        // 결과를 보여 준다
        printf("결과는: %f\n", result);

}


=> 특별히 잘못된 부분은 없어 보입니다. LoadLibrary 함수 호출시 dll 파일명이 제대로 되어 있는지
     다시한번 체크를 하는게 좋겠습니다.


- 묵시적 실행 소스 코드 -


#include <stdio.h>

#include "my_Math.h" // dll 의 헤더 포함


#pragma comment(lib, "my_Math.lib")  // 필요한 함수가 어떤 dll 에 있는지 알려주는 역할(링크시 필요)


// dll 의 함수의 원형을 이렇게 하나씩 선언해도 되긴 하지만, 함수의 수가 많아지면

// 이는 굉장히 귀찮은 작업이 됩니다. (my_Math.h 포함하는 것으로 이를 방지함)

/*

extern "C" __declspec(dllimport) int my_add(int a,int b);
extern "C" __declspec(dllimport) int my_sub(int a,int b);
extern "C" __declspec(dllimport) int my_mul(int a,int b);
extern "C" __declspec(dllimport) int my_div(int a,int b);

*/


int main()
{
   int a=3,b=5;

 printf("%d",my_add (a,b));

// return a+b;
 printf("%d",my_sub (a,b));

// return a+b;
 my_mul (a,b);

// return a+b;
my_div (a,b);

 return 0;
}



반응형
반응형

http://goo.gl/MRBRR




프로그램, 프로세스, 스레드의 차이를 알아보자.

일반적으로 프로그램(Program)은 메모장과 같은 하나의 실행 가능한 단위를 의미한다. 

그러나 아시다시피 메모장은 여러개를 동시에 실행할 수 있다. 이때 각각의 메모장은 서로 다른 프로세스(Process)에 의해서 실행이 된다.

즉, 프로세스는 프로그램을 객체화 시킨 것.

각 프로세스는 4GB의 개별 주소 공간과 파일, 메모리, 스레드를 소유하게 된다. 이 말에서도 나온것 처럼 스레드는 다시 프로세스에 속한다.

하나의 프로세스는 여러개의 스레드(Thread)를 소유할 수 있으며 프로세스 자체는 껍데기일 뿐이고 사실은 스레드가 모든 명령을 수행한다.

프로세스가 최초로 생성될때 각종 관련 변수와 메모리 등을 생성하면서 메인 스레드를 생성하게 되는데 이 메인 스레드가 사실 모든 명령들을 처리한다.

그러나 사용자가 원하면 멀티 스레드 (Multi-Thread)가 가능한데 즉, 하나의 프로세스가 여러개의 스레드를 가지고서 다양한 작업을 분산 시켜 처리하도록 하는 것이다.

사용자가 임의로 타이머를 만들어서 규칙적인 시간에 일정한 작업을 처리하는 것보다 (또는 PeekMessage를 이용한 방법보다) 운영체제가 알아서 시간을 쪼개에 스레드에게 작업을 나누어 주는 것이 훠어어얼씬 효율적이고 빠르단다.

다만, 멀티 스레드가 가지는 문제점은 하나의 프로세스안에 존재하는 스레드들은 같은 코드와 주소공간, 그리고 전역 변수를 '공유'하게 됨으로써 생기는 '자원 경쟁' 혹은 '비동기 문제'이다.

하나의 프로세스 안에 있는 1번 스레드가 전역 변수를 변경하고 있는 동안 다른 2번 스레드가 그 값을 참조하거나 다시 변경하게되면 사용자가 예상치 못한 이상한 값들로 바뀌어 버리는 문제가 생기게 된다.

그래서 이것을 해결하기 위해서는 가장 좋은 것이 스레드들이 공유하는 전역 변수 자체를 안만들거나, 혹은 스레드 지역 기억장소 (Thread Local Storage) 공간을 사용해야 한다.

TLS는 다른 말로 쓰자면 '하나의 스레드에 대해서만 전역 변수인 공간'이다. 

이렇게 복잡하게 설정을 해야 하는 이유는, C의 런타임 라이브러리가 애당초 Multi-Thread개념이 생기기 전에 만들어 졌기 때문이다.

기존 C 런타임 라이브러리는 함수들이 주로 Static 을 많이 사용하고 있는데 (특정 함수에서) 이 Static 변수들이 스레드 간에 '자원 경쟁' 혹은 '비동기' 때문에 엉뚱한 결과를 내는 현상을 가져왔다.

그래서 이걸 뜯어고치기되 기존 C 런타임 라이브러리를 동시에 유지하기 위해서 MLS가 도입된 것이다.


반응형

+ Recent posts