특히 async
Ado.Net 및 EF 6과 함께 모든 곳에서 사용 하고 있기 때문에이 질문이 매우 흥미 롭습니다.이 질문에 대한 설명을 누군가에게 바라고 있었지만 일어나지 않았습니다. 그래서 나는이 문제를 내 편에서 재현하려고했습니다. 나는 여러분 중 일부가이 흥미로운 것을 발견하기를 바랍니다.
첫번째 좋은 소식 : 나는 그것을 재현했다 :) 그리고 그 차이는 엄청나 다. 요인 8로 ...
우선은 처리 뭔가를 의심했다 CommandBehavior
이후, 나는 흥미로운 기사 읽기 에 대해 async
이런 말을, 아도로를 :
"비 순차 액세스 모드는 전체 행에 대한 데이터를 저장해야하기 때문에 서버에서 큰 열을 읽는 경우 (예 : varbinary (MAX), varchar (MAX), nvarchar (MAX) 또는 XML) 문제가 발생할 수 있습니다. ). "
나는 ToList()
전화를 CommandBehavior.SequentialAccess
하고 비동기 전화를해야한다고 의심했습니다 CommandBehavior.Default
(비 순차적 인 문제가 발생할 수 있음). 그래서 EF6의 소스를 다운로드하고 어디서나 ( CommandBehavior
사용 하는 곳) 중단 점을 두었습니다 .
결과 : 없음 . 모든 호출은 CommandBehavior.Default
.... 로 수행됩니다 . 그래서 나는 EF 코드로 들어가서 무슨 일이 일어나는지 이해하려고 노력했습니다 ..... 우 우치 ... 나는 그런 위임 코드를 보지 못했습니다.
그래서 무슨 일이 일어나고 있는지 이해하기 위해 프로파일 링을 시도했습니다 ...
그리고 나는 무언가가 있다고 생각합니다 ...
다음은 내가 벤치마킹 한 테이블을 생성하는 모델이며, 그 안에 3500 개의 라인이 있고 각각에 256KB의 랜덤 데이터가 있습니다 varbinary(MAX)
. (EF 6.1-CodeFirst- CodePlex ) :
public class TestContext : DbContext
{
public TestContext()
: base(@"Server=(localdb)\\v11.0;Integrated Security=true;Initial Catalog=BENCH") // Local instance
{
}
public DbSet<TestItem> Items { get; set; }
}
public class TestItem
{
public int ID { get; set; }
public string Name { get; set; }
public byte[] BinaryData { get; set; }
}
테스트 데이터를 작성하고 EF를 벤치 마크하는 데 사용한 코드는 다음과 같습니다.
using (TestContext db = new TestContext())
{
if (!db.Items.Any())
{
foreach (int i in Enumerable.Range(0, 3500)) // Fill 3500 lines
{
byte[] dummyData = new byte[1 << 18]; // with 256 Kbyte
new Random().NextBytes(dummyData);
db.Items.Add(new TestItem() { Name = i.ToString(), BinaryData = dummyData });
}
await db.SaveChangesAsync();
}
}
using (TestContext db = new TestContext()) // EF Warm Up
{
var warmItUp = db.Items.FirstOrDefault();
warmItUp = await db.Items.FirstOrDefaultAsync();
}
Stopwatch watch = new Stopwatch();
using (TestContext db = new TestContext())
{
watch.Start();
var testRegular = db.Items.ToList();
watch.Stop();
Console.WriteLine("non async : " + watch.ElapsedMilliseconds);
}
using (TestContext db = new TestContext())
{
watch.Restart();
var testAsync = await db.Items.ToListAsync();
watch.Stop();
Console.WriteLine("async : " + watch.ElapsedMilliseconds);
}
using (var connection = new SqlConnection(CS))
{
await connection.OpenAsync();
using (var cmd = new SqlCommand("SELECT ID, Name, BinaryData FROM dbo.TestItems", connection))
{
watch.Restart();
List<TestItem> itemsWithAdo = new List<TestItem>();
var reader = await cmd.ExecuteReaderAsync(CommandBehavior.SequentialAccess);
while (await reader.ReadAsync())
{
var item = new TestItem();
item.ID = (int)reader[0];
item.Name = (String)reader[1];
item.BinaryData = (byte[])reader[2];
itemsWithAdo.Add(item);
}
watch.Stop();
Console.WriteLine("ExecuteReaderAsync SequentialAccess : " + watch.ElapsedMilliseconds);
}
}
using (var connection = new SqlConnection(CS))
{
await connection.OpenAsync();
using (var cmd = new SqlCommand("SELECT ID, Name, BinaryData FROM dbo.TestItems", connection))
{
watch.Restart();
List<TestItem> itemsWithAdo = new List<TestItem>();
var reader = await cmd.ExecuteReaderAsync(CommandBehavior.Default);
while (await reader.ReadAsync())
{
var item = new TestItem();
item.ID = (int)reader[0];
item.Name = (String)reader[1];
item.BinaryData = (byte[])reader[2];
itemsWithAdo.Add(item);
}
watch.Stop();
Console.WriteLine("ExecuteReaderAsync Default : " + watch.ElapsedMilliseconds);
}
}
using (var connection = new SqlConnection(CS))
{
await connection.OpenAsync();
using (var cmd = new SqlCommand("SELECT ID, Name, BinaryData FROM dbo.TestItems", connection))
{
watch.Restart();
List<TestItem> itemsWithAdo = new List<TestItem>();
var reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess);
while (reader.Read())
{
var item = new TestItem();
item.ID = (int)reader[0];
item.Name = (String)reader[1];
item.BinaryData = (byte[])reader[2];
itemsWithAdo.Add(item);
}
watch.Stop();
Console.WriteLine("ExecuteReader SequentialAccess : " + watch.ElapsedMilliseconds);
}
}
using (var connection = new SqlConnection(CS))
{
await connection.OpenAsync();
using (var cmd = new SqlCommand("SELECT ID, Name, BinaryData FROM dbo.TestItems", connection))
{
watch.Restart();
List<TestItem> itemsWithAdo = new List<TestItem>();
var reader = cmd.ExecuteReader(CommandBehavior.Default);
while (reader.Read())
{
var item = new TestItem();
item.ID = (int)reader[0];
item.Name = (String)reader[1];
item.BinaryData = (byte[])reader[2];
itemsWithAdo.Add(item);
}
watch.Stop();
Console.WriteLine("ExecuteReader Default : " + watch.ElapsedMilliseconds);
}
}
일반적인 EF 호출 ( .ToList()
)의 경우 프로파일 링이 "정상"으로 보이고 읽기 쉽습니다.
여기서 우리는 스톱워치로 8.4 초를 얻습니다 (프로파일 링은 성능을 느리게합니다). 또한 호출 경로를 따라 HitCount = 3500을 발견하는데, 이는 테스트의 3500 줄과 일치합니다. TDS 파서 쪽에서 TryReadByteArray()
는 버퍼링 루프가 발생하는 메서드 에 대한 118 353 호출을 읽음으로써 상황이 악화되기 시작했습니다 . ( byte[]
256kb 각각 에 대한 평균 33.8 호출 )
이 async
경우에는 실제로 다릅니다 .... 먼저 .ToListAsync()
통화가 ThreadPool에서 예약 된 후 대기합니다. 여기서 놀라운 것은 없습니다. 그러나 이제 async
ThreadPool 의 지옥은 다음과 같습니다.
첫째, 첫 번째 경우 전체 통화 경로를 따라 3500 개의 적중 횟수를 가졌으며 여기에는 118 371이 있습니다. 또한 스크린 샷에 넣지 않은 모든 동기화 호출을 상상해야합니다 ...
둘째, 첫 번째 경우에는 TryReadByteArray()
메소드 에 대한 "단지 118 353"의 호출이 있었으며 여기에는 2 050 210의 호출이 있습니다! 17 배 더 ... (대용량 1Mb 어레이 테스트에서 160 배 더 큼)
또한 있습니다 :
- 120 000 개의
Task
인스턴스가 생성됨
- 727 519
Interlocked
전화
- 290 569
Monitor
전화
ExecutionContext
264 481 캡처의 98 283 인스턴스
- 208 733
SpinLock
전화
내 생각에 버퍼링은 비동기 방식으로 이루어지며 병렬 작업은 TDS에서 데이터를 읽으려고합니다. 이진 데이터를 구문 분석하기 위해 너무 많은 작업이 생성되었습니다.
예비 결론으로, Async는 훌륭하고 EF6은 훌륭하지만 현재 구현에서 EF6의 비동기 사용은 성능 측면, 스레딩 측면 및 CPU 측면에 큰 오버 헤드를 추가합니다. 8 ~ 10 배 더 긴 작업 ToList()
의 ToListAsync
경우 사례와 20 % . 나는 오래된 i7 920에서 실행합니다).
몇 가지 테스트를 수행하는 동안 이 기사에 대해 다시 생각하고 있었고 놓친 것이 있습니다.
".NET 4.5의 새로운 비동기 메서드의 경우 동작이 주목할만한 예외 하나를 제외하고 동기 메서드와 정확히 동일합니다. 비 순차 모드의 ReadAsync."
뭐 ?!!!
나는 Ado.Net 일반 / 비동기 호출에서, 그리고에 포함 할 내 벤치 마크를 확장 그래서 CommandBehavior.SequentialAccess
/ CommandBehavior.Default
, 여기에 큰 놀라움입니다! :
우리는 Ado.Net과 똑같은 동작을합니다 !!! 페이스 팜 ...
내 결론은 EF 6 구현에 버그가 있다는 것입니다. 그것은 전환해야 CommandBehavior
에 SequentialAccess
비동기 호출이 들어있는 테이블 위에 이루어질 때 binary(max)
열. 너무 많은 작업을 생성하고 프로세스 속도를 저하시키는 문제는 Ado.Net 측에 있습니다. EF 문제는 Ado.Net을 사용하지 않는 것입니다.
이제 EF6 비동기 메소드를 사용하는 대신, 비 비동기 방식으로 EF를 호출 한 다음 a TaskCompletionSource<T>
를 사용 하여 결과를 비동기 방식으로 리턴하는 것이 좋습니다.
참고 1 : 부끄러운 오류로 인해 게시물을 편집했습니다 .... 로컬이 아닌 네트워크를 통해 첫 번째 테스트를 수행했으며 제한된 대역폭으로 인해 결과가 왜곡되었습니다. 업데이트 된 결과는 다음과 같습니다.
참고 2 : 테스트를 다른 사용 사례 (예 : nvarchar(max)
많은 데이터 사용)로 확장하지는 않았지만 동일한 동작이 발생할 가능성이 있습니다.
참고 3 :이 ToList()
경우 일반적으로 12 % CPU (내 CPU의 1/8 = 논리 코어 1)입니다. ToListAsync()
스케줄러가 모든 트레드를 사용할 수없는 것처럼 이례적인 경우는 최대 20 %입니다 . 아마도 너무 많은 Task가 생성되었거나 TDS 파서의 병목 현상으로 인한 것 같습니다.