프로그래밍 일반 2015. 5. 20. 18:02

컨테이너 순환문에서는 전위 증가 연산자를 사용하자

링크 : 어셈블리 기본


int i = 0;

int iPrefix = ++i; // 전위연산

int iPostfix = i++; // 후위연산


해당 코드의 결과는 프로그래머라면 누구나 알 수 있을 것이다.


그런데 두 연산자는 속도의 차이가 있을까?


필자는 전위 연산자가 더 빠르다고 배웠기 때문에 습관적으로 전위 연산자를 사용하고 있었는데 정말로 그런지 어셈코드를 확인해 보기로 했다.





코드를 보면 알 수 있지만 대입할 때 연산자 구현 차이[각주:1]만 있을 뿐 속도차이는 없다는 것을 알 수 있다.


그렇다면 객체에서는 어떨까?




객체의 경우 전위 연산자 처리가 훨씬 단순하다는 사실을 알 수 있다. 단지 스택 메모리를 추출 후 서브루틴으로 분기한다.


그에비해 후위 연산자는 상대적으로 처리가 복잡하고 코드를 잘 보면 컨테이너 소멸자까지 호출되고 있다.


Why??


std::list 코드를 열어보면




전위 연산자는 자신을 증가 시킨 후 참조형으로 리턴하고 후위 연산자는 임시 개체에 상태를 복사한 후 자신을 증가 시키고 


복사된 임시 객체를 리턴 시키고 있다. 어셈코드가 설명이 되는 부분이다.


결론 int, float, char 등 내부 타입의 경우 성능상 차이가 없지만 객체는 전위 연산자를 사용하는게 더 빠르다.


  1. 증가 후 대입 대입 후 증가 [본문으로]
SQL 2015. 5. 20. 13:04

Loop 쿼리를 제거하고 One 쿼리를 작성해야 하는 이유

링크 : 

  1. 1. 데이터 파일의 구조
  2. 2. DBMS 메모리 구조
  3. 3. 인덱스 기본
  4. 4. 정렬을 제거하는 방법
  5. 5. 프로시져 캐시 경합 확인하기
  6. 6. Loop 쿼리 제거하기
  7. 7. 튜닝 결과 샘플 보기




데이터베이스 작업 요청에 따른 Call 의 종류를 살펴보면

  • Parse Call : SQL 파싱을 요청하는 Call
  • Execute Call : SQL 실행을 요청하는 Call
  • Fetch Call : SELECT 문의 결과 데이터 전송을 요청하는 Call

일반적으로 서버에서 DB에 쿼리를 요청하는 코드는 


WorkThread::Func()

{

……

…..

Lib.api_call(_T(“실행 쿼리”));

Blocking...

}


응답이 돌아올 때까지 해당 쓰레드는 Blocking 상태로 다음 작업을 할 수 없다.


만약 바인딩 변수로 대상 아이템의 시퀀스를 받아 해당 아이템 정보를 얻는 쿼리가 있다고 가정하고 이 쿼리를 이용하여 아이템 30개의 정보를 얻는 서버 로직을 생각해보자.


일반적으로 아래와 같은 형식으로 작업 할 것이다.


WorkThread::SelectFunc()

{

    ...

    

    // Loop Call

    for (int i = 0; i < 30; ++i)

    {

        Lib.api_call(_T("select 쿼리"));

        Blocking...

    }

}


이런 형태의 코드는 DBMS 내부적으로 Parse Call, Execute Call 이 각각 30회씩 발생하고, 추가로 30회의 응답 대기가 발생한다.


Loop 쿼리는 대부분의 시간을 네트워크 구간에서 소비하거나 Call 이 발생할 때마다 OS로부터 CPU와 메모리 리소스를 할당 받으려고 대기하면서 소비한다.


만약 One 쿼리를 만들어 실행 시킨다면


WorkThread::SelectFunc()

{

    ...


// One Call

Lib.api_call(_T(“select 쿼리”));

      Blocking...    

}


Parser Call, Execute Call 이 각각 1회씩만 발생된다. 추가로 가장 큰 이득은 Blocking 상태 (네트워크 구간 + 리소스 할당 대기 + 쿼리 처리)가 1회로 끝나는 것이다.


One SQL은  대상 그룹을 문자열로 넘겨 받아 파싱 후 테이블 변수로 만들어 조인한다.


만약 테이블 변수와 조인을 하게 되는 대상 테이블이 동일한 페이지에 정렬되어 Page 접근이 적어진다면[각주:1] 조인 효율은 극대화 될 것이다.


One Call 쿼리에 30개의 대상 아이템 시퀀스를 문자열로 넘긴다면 대략  "1, 2, ..., 30"  이런 형식이 될 테고


해당 문자열을 파싱해서 테이블 변수로 만드는 쿼리는 


@strArray 는 바인딩 변수


SET NOCOUNT ON;

-- 테이블 변수 가능한 작게 유지할 필요가 있다.

DECLARE @tTemp TABLE (

Seq BIGINT

);

DECLARE @Seq BIGINT = 0;

    WHILE CHARINDEX(',', @strArray) <>0

BEGIN

SET @Seq = CAST(SUBSTRING(@strArray, 0, CHARINDEX(',', @strArray)) AS BIGINT)

SET @strArray = SUBSTRING(@strArray, CHARINDEX(',', @strArray) + 1, LEN(@strArray) - CHARINDEX(',', @strArray))

INSERT INTO @tTemp

SELECT @Seq

END

    

    IF 0 < LEN(@strArray)

BEGIN

SET @Seq = 0

SET @Seq = CAST(@strArray AS BIGINT)

INSERT INTO @tTemp

SELECT @Seq

END

SELECT 

...

FROM @tTemp AS O

INNER JOIN dbo.targetTable AS I

ON O.Seq = I.Seq

WHERE 튜닝 규칙을 만족하는 범위 한정 조건절 작성



대략 이런 형식이 될 것이다.


만약 테이블 변수를 사용하지 않고 파싱 중간중간 결과를 Select 한다면 TDS 패킷 수량이 많아진다. 


즉 DB에서 서버에 전달하는 패킷이 더 많아지기 때문에 불필요한 비용이 발생할 수도 있다. 


조인 대상 테이블 구조에 따라 랜덤 액세스 비용이 발생할 수도 있기 때문에 꼼꼼히 살펴볼 필요는 있지만 Blocking 상태에서 소모되는 시간보다는 훨씬 적을 것이다.


상대적인 IO 비용


 IO

비용 

L1 cache

3 cycles 

L2 cache

14 cycles 

RAM

250 cycles 

DISK 

41000000 cycles 

Network 

240000000 cycles 








  1. DB는 하나의 레코드에 접근하더라고 레코드가 포함된 Page를 메모리에 로드한 후 로드된 메모리 블럭을 통해 레코드에 접근한다. [본문으로]
SQL 2015. 5. 4. 10:18

프로시저 캐시 분석 쿼리


-- 프로시저 캐시 경합을 모니터링 할 수 있는 정보를 출력한다.

SELECT text, objtype, refcounts, usecounts, size_in_bytes, disk_ios_count, context_switches_count, 

           pages_allocated_count, original_cost, current_cost 

FROM sys.dm_exec_cached_plans p

CROSS APPLY sys.dm_exec_sql_text(plan_handle) 

JOIN sys.dm_os_memory_cache_entries e

ON p.memory_object_address = e.memory_object_address 

WHERE cacheobjtype = 'Compiled Plan' AND type in ('CACHESTORE_SQLCP', 'CACHESTORE_OBJCP')






컬럼을 노출하지 않기 위해 블라인드 처리한 부분들이 있지만 해당 내용은 동일한 동작을 하는 쿼리가 리터럴 SQL로 작성되어 프로시저 캐시를 서로 경합하면서 점유하고 있는 상태의 극히 일부분이다. 대부분 동적 쿼리를 만들면서 바인딩 변수를 매핑하지 않아 생기는 버그들이다. 아니면 관리툴에서 날짜를 스트링으로 처리하던가하는 경우도 있다.

text와 objtype를 참고하여 캐시 경합을 발생 시키는 쿼리들을 찾아서 문제를 제거하자.



-- 실행계획 캐시를 비운다.

dbcc freesystemcache('SQL Plans');

go

쿼리 수정 후 문제가 해결되었는지 확인하기 위해 프로시저 캐시의 실행계획을 삭제한다. 일시적으로 하드파싱이 일어나는 성능 저하가 발생하지만 어차피 우리가 원하는 일이다.

다시 프로시저 캐시를 모니터링 하면서 실행계획이 재사용 가능한지 adhoc 쿼리가 또 발생하는지 등 원하는 결과가 나올 때까지 개선하도록 한다.


관련 링크 : 

sys.dm_exec_cached_plans

sys.dm_os_memory_cache_entries

freesystemcache