책을 읽다가 좀 정리함.
책의 내용은 좋은데... 이해가...
http://www.yes24.com/24/goods/24020688
성능에 대한 부분에 대해서 고민을 하고 있거나
C#, .NET 프로그래밍 시 이유나 원인을 잘 모르고 있던 것들에 대해서 설명을 잘해주고 있다.
게다가 어셈블리까지 직접 보여주며 설명하고 있는 센스 :) (난 이해 못함)
내용이 어렵거나 번역상 몇 번을 읽어도 이해되지 않는 부분이나
다소 불필요할 정도로 자세한 내용도 있는 듯하다.
보고 대체로 나에게 필요한 부분만 정리함.
자세하고 중요한 내용은 다 책에 있으니 나중에 계속 읽어 보련다.
비동기 프로그래밍
멀티 스레드를 사용하는 3가지 근본적인 이유
- UI main thread를 차단?하지 않기 위해
- I/O를 완료할 때까지 기다리지 않고 다른 작업을 함께 처리
- 처리하는 작업에 모든 프로세스를 동원
Thead context switching 비용을 위해서 .NET에서는 각 관리되는 프로세스에 대한 스레드 풀을 관리함.
Task 사용
.NET 4.0에서는 TPL(Task Paralle Library)라는 스레드의 추상화를 소개함.
내부적으로 TPL은 .NET thread pool을 사용하지만 해당 thread를 pool에 다시 반환하기 전에 동일한 thread에서 순차적으로 여러 tasks를 실행하므로 더 효율적으로 수행한다.
- 연속 Task가 빠르고 짧은 코드 부분이면 task들이 동일 스레드에서 실행되도록 지정하는 것도 방법
: context switching, queue에서 대기 시간을 줄일 수 있음.
: https://msdn.microsoft.com/ko-kr/library/dd321576(v=vs.110).aspx
public Task ContinueWith(
Action<Task> continuationAction,
TaskContinuationOptions continuationOptions
)
public class TaskCounter
{
private volatile int _count;
public void Track(Task t)
{
if (t == null) throw new ArgumentNullException("t");
Interlocked.Increment(ref _count);
t.ContinueWith(ct => Interlocked.Decrement(ref _count), TaskContinuationOptions.ExecuteSynchronously);
}
public int NumberOfActiveTasks { get { return _count; } }
}
: 단 I/O thread와 관련되어 있다면 확인 필요.
- 오랫동안 동작하는 Task의 경우 TaskCreationOptions.LongRunning flag를 사용
: https://msdn.microsoft.com/ko-kr/library/system.threading.tasks.taskcreationoptions(v=vs.110).aspx
- Task 다중 연속
: ContinueWith()를 사용해서 연속해서 사용할 수도 있음.
: https://msdn.microsoft.com/ko-kr/library/dd270696(v=vs.110).aspx
task.ContinueWith(OnTaskEnd1);
task.ContinueWith(OnTaskEnd2);
=> OnTaskEnd1, OnTaskEnd2 가 독립적이고 병렬적으로 실행됨.
task.ContinueWith(OnTaskEnd1).ContinueWith(OnTaskEnd2);
=> OnTaskEnd1이 실행되고 OnTaskEnd2가 실행됨.
: Task 종료 조건에 따라 실행되게 할 수도 있음.
. https://msdn.microsoft.com/ko-kr/library/dd321576(v=vs.110).aspx
. https://msdn.microsoft.com/ko-kr/library/system.threading.tasks.taskcontinuationoptions(v=vs.110).aspx
: 다중 Task handling을 위해서는 Task.Factory.ContinueWhenAll, Task.Factory.ContinueWhenAny
- Task를 강제로 취소하는 것 보다 Task 내에서 취소 여부를 확인해서 종료하는 방법 추천
- Task.Wait()을 사용하는 것은 Task를 강제로 기다리게 하므로 비추임.
: 해당 thread가 대기 상태로 차단되고 언제 다시 시작할 지 몰라서 다른 스레드를 실행 시킴.
: 스레드에서 신호를 기다리면서 몇 밀리초 동안 회전하는 동기화 개체를 적중 시킴. 실패 시 차단되고 기다림.
Async와 Await
.NET 4.5에서 async, awit가 소개됨. TPL 코드를 쉬운 선형 동기화 코드로 전환 시킴.
: https://docs.microsoft.com/ko-kr/dotnet/csharp/async
private readonly HttpClient _httpClient = new HttpClient();
downloadButton.Clicked += async (o, e) =>
{
var stringData = await _httpClient.GetStringAsync(URL);
DoSomethingWithData(stringData);
};
올바른 Timer 사용
- 과도하게 많은 Timer 를 만들지 않도록 하자.
: 모든 Timer는 스레드 풀의 단일 스레드에서 제공됨.
: 너무 많은 Timer 는 Timer Callback 실행에 지연을 일으킨다.
- 운영 체제의 tick counter(15밀리초)보다 정밀할 수 없음.
스레드를 중단하지 않는다.
스레드 우선순위를 변경하지 않는다.
스레드 동기화와 잠금
- 스레드들 간 동기화/잠금은 CPU를 낭비하며 context switching 시간을 높힌다.
- 다음 기본 원칙들을 고려하자.
: 잠금이 필요한가?
. 스레드 동기화에 대한 필요성을 완전히 제거할 수 있다면 성능향상에 있어 궁극적인 방법임.
. 단지 변수를 읽기만하거나 불변일 경우 동기화는 필요하지 않다.
. 쓰기를 할 경우 임시사본을 사용해서 쓴 다음 한번에 동기화하여 쓰는 것도 방법이다.
: 동기화 우선 순위
. 동기화에도 여러 수준이 있으므로 판단해서 사용
성능에 미치는 순서 : 동기화 안함 < Interlocked method < lock/moitor class < 비동기 잠금 < 기타
: 메모리 모델
. 메모리 모델 : 시스템(HW/SW)에서 규칙의 집합으로 컴파일러나 프로세서에서 다중 스레드에 다시 순서를 매기는 읽기와 쓰기 작업이 얼마나 되는지를 통제한다.
순서 재정렬에서 컴파일러와 하드웨어가 많은 최적화를 수행하지 못하게 하는 절대적인 제약 사항이 있는 경우 strong 모델로 기술한다.
weak 모델에서는 컴파일러와 프로세서가 더 많은 자유를 허용 받으므로 잠재적으로 더 나은 성능을 얻기 위해 읽기와 쓰기 명령들의 순서를 바꿀 수 있다.
. x86/x64은 strong 모델이며 ARM은 weak 모델을 가진다.
=> 즉 CLR의 호환성을 위해 해당 코드가 약한 메모리 모델이 맞는지 보장하는 것이 좋은 방법이다.
스레드 간의 공유되는 상태에서 volatile을 올바로 사용하여 JIT 컴파일러로 하여금 문제를 정리하도록 하는 것도 방법임.
공유 상태의 적절한 순서를 보장하는 다른 방법은 Interlocked를 사용해서 잠금 내에서 모든 액세스를 유지하는 것도 방법임.
* 나머지는 책에서 자세히 다루고 있음 꼭 책을 구입해서 보세요!!!
일반 코딩 및 클래스 설계
- 클래스의 인스턴스는 항상 힙에 할당,일부 개체는 고정 오버헤드를 가짐 (32bit - 8bytes, 64bit - 16bytes)
- 구조체는 전혀 오버헤드를 가지지 않음. (메모리 사용 = 구조체의 모든 필드의 크기 합)
: 구조체가 메서드의 지역변수로 선언 => 스택 할당
: 구조체가 한 클래스의 일부분이면 => 힙에 존재
- 구조체 배열에서는 데이터의 동일한 크기가 메모리 양을 작게 차지한다. (오버헤드 없음)
- 구조체 배열은 순차적으로 메모리에 존재하므로 CPU 캐시에 존재 시 더 빠른 순서로 참조 될 수 있음.
- 메서드를 가상화 하는 것은 JIT 컴파일러가 메서드를 인라인 시키는 특정 최적화를 막는다.
: 파생 클래스가 없다면 sealed를 표시하라.
- foreach보다 for가 빠르다. (당연하겠지만..)
: IEnumerable<int>보다 단순 배열이 더 많은 비용이 소요된다. (당연하겠지만..)
- 캐스팅도 비용이다.
: if (a is foo)
{
Foo f = (Foo)a;
}
=>
Foo f = a as Foo;
if (f != null)
{
}
- 예외가 발생된 메서드는 단순 빈 메서드보다 수천 배 느리다
.NET 프레임워크 사용
- Generic Collection은 boxing이나 변환 비용을 발생시키지 않고 더 좋은 메모리 집약성을 가짐.
: https://docs.microsoft.com/ko-kr/dotnet/csharp/programming-guide/generics/
: Dictionary => hash table로 구현되고 O(1) 삽입, 조회 시간을 가짐
: SortedDictionary => binary search tree로 구현되고 O(log n) 삽입과 조회 시간을 가짐
: SortedList => 정렬된 배열로 구현 O(log n) 조회시간, worst case로 O(n) 삽입 시간을 가짐, 단 최소 메모리 사용.
: HashSet => hast table을 사용, O(1) 삽입 및 제거 시간 가짐
: SortedSet => binary search tree를 사용하고 O(long n) 삽입, 제거 시간을 가짐
- 문자열 비교 시 option에 따라서 속도 차이 고려 필요
: String.Compare(a,b, option), StringComparison.OrdinalIgnoreCase < StringComparison.Ordinal < StringComparison.CurrentCulture
: https://docs.microsoft.com/ko-kr/dotnet/csharp/programming-guide/strings/how-to-compare-strings
- ToLower, ToUppper는 왠만하면 피하라.
- String 생성 시 단순 '+' 연산자나 String.Concat 메서드를 사용하라.
: StringBuilder를 사용하는 것 보다 효과적이고 StringBuilder는 문자열이 아주 가변적이고 크기가 클 때 고려하라.
- 문자열 서식 (String.Format()) 메서드는 비용이 크므로 간단한 문자열이면 단순 연결 '+'을 사용하라.
- 정상적인 상황에서 예외 발생 API를 회피하라.
- 느슨한 초기화를 사용하자 (Lazy<T>)
- 정규 표현식은 빠르지 않고 어셈블리 생성, JIT 비용, 평가 시간을 위한 비용 및 시간이 필요하다.
: Regex 성능 개선을 위해서는 Regex 인스턴스 변수 생성, RegexOptions.Compiled 플래그 사용, 반복 생성하지 않고 재사용 하는 것이 방법
- LINQ는 편리하고 성능 기준에 만족하지만 코드 숨김의 가능성이 있어 조심해서 사용하라.
댓글 없음:
댓글 쓰기