memcpy 와 pointer shift 연산의 속도 차이의 이유

이성일@Google의 이미지

안녕하세요,

프로그램 작업을 하다가 알게 된

쉬프트 연산과 메모리 카피 부분의 속도 차이가 왜 발생하는지 잘 이해가 되지 않습니다.

for(i=0; i {
for(j=0, k=0; j {
//쉬프트 연산
piDest[j] = (pImage[k + 3] << 24) + (pImage[k + 2] << 16) + (pImage[k + 1] << 8) + (pImage[k]);

//memcpy
memcpy(piDest+z, pImage+k,4); // 1byte씩 복사를 한다.
z += 4;
}
//쉬프트 연산시 사용
piDest += nWidth;
pImage += nByteWidth;
}

Stephen Kyoungwon Kim@Google의 이미지

전자는 연속된 네 바이트를 하나씩 가져다가 각각 int로 캐스팅 한 뒤 각각 0, 8, 16, 24 비트만큼 쉬프팅 하고, 그 네 가지 값을 차례로 더하는 것입니다. 후자는 단순히 pImage+k 주소의 4 바이트를 복사해서 다른 곳으로 옮기는 거고요. 어떻게 복사할지는 온전히 컴파일러 혹은 라이브러리 구현에 맡겨집니다.

쉬프트를 하는 크기로 보아 8 bit 짜리 데이터인 것 같은데 보통의 머신은 32비트 또는 64비트 연산이 기본이라서 4바이트를 카피할 때, alignment가 맞다면 네 바이트를 한 번에 옮깁니다.

일반적으로 메모리 상에서 연속된 N개 카피하겠다고 memcpy를 부르는 경우에는 컴파일러나 라이브러리가 최적화할 여지가 많습니다. 그런데 element 하나씩 가져다 일일이 쉬프팅을 해서 조합하는 경우에는, 첫째, 컴파일러가 이 코드의 의도가 네 개씩 모아 하나의 큰 데이터로 조합하려고 한다는 점을 이해해야 하고요, 둘째, 이 과정에서 행여 생길 수도 있는 사이드 이펙트 (overflow bit을 셋팅한다거나)가 없거나 있어도 안전하다는 것을 보장해야 하므로 더 어렵습니다.

이성일@Google의 이미지

for(i=0; i {
for(j=0, k=0; j {
// Pointer 연산
//piDest[j] = (pImage[k+3]<<24) + (pImage[k+2]<<16) + (pImage[k+1]<<8) + (pImage[k]);

//memcpy
//memcpy(piDest + z, pImage + k, 4); // 1byte씩 복사를 한다.
//z += 4;

//char 연산
piDest[j] = (pImage[k]);
piDest[j+1] = (pImage[k + 1]);
piDest[j+2] = (pImage[k + 2]);
piDest[j+3] = (pImage[k + 3]);
}
piDest += nWidth;
pImage += nByteWidth;
}

수행되는 순서를 코드 순서대로 돌려보니 각각의 수행 속도는 아래와 같았습니다.

Pointer : 0.02670

Memcpy : 0.04243

char : 0.03263

* for을 통해서 10bit 이미지를 8bit이미지로 변환하는 코드입니다.
* k 변수값을 통해 10bit 이미지의 5번째 값을 버려야 합니다.

-> int 포인터로 수행하게되면 한번에 4개의 byte에 접근하여 데이터를 제어하여 piDest의 공간에 넣을수 있다. (단 엔디언의 영향으로 쉬프트를 한다.)

memcpy또한 동일한 기능으로 알고 있습니다.

-> char형을 통하면 엔디언의 영향을 받지는 않지만 메모리에 4번 접근해야함으로 속도가 느리다.

이렇게 이해를 하고 있습니다. 알려주신 답변을 토대로 추가로 이해한 내용은

memcpy를 수행하면 컴파일러나 라이브러리가 스스로 판단하여 최적의 방법으로 복사를 수행하고, 쉬프트 연산을 하게 되면 개발자에 의하여 지정된 방식으로밖에 수행할수 없다, (컴퓨터가 최적의 방식을 찾는 시간떄문에 속도가 느린것으로 이해 됩니다.)

질문이 너무 중구 난방이라 이해가 잘 되실지 모르겠습니다.

코드를 이쁘게 적었다고 생각해도, 이상하게 들여쓰기가 적용되지 않네요..

xtiinhs의 이미지

Quote:
코드를 이쁘게 적었다고 생각해도, 이상하게 들여쓰기가 적용되지 않네요..

댓글 입력하기 전에 입력란에 뭐라고 쓰여 있는지 한 번 보셨나요?

Stephen Kyoungwon Kim의 이미지

일단 질문이 잘 이해가 되지 않는데요. pImage와 piDest의 타입이 뭔가요? char랑 int 종류 아닌가요? 아래쪽 코드를 보니 piDest가 char류의 어레이로 쓰인 것 같네요. 어느 쪽인가요?

그리고 루프 끝나기 전에 포인터를 옮기는 부분을 전에 답을 달 때 주의깊게 보지 않았습니다. 연속된 메모리를 카피하는 게 아니군요.

핵심은 논리적으로 똑같은 일을 하는 소스 코드라도 저수준에서는 다르게 보일 수 있고 그게 성능 차이로 이어질 수 있다는 것입니다.

memcpy는 이 사례에서 아마도 매번 함수 호출을 할 텐데 느려질 것 같구요.

느려질 "수밖에 없는" 것은 아니고 컴파일러 최적화에 달려 있겠죠. 다만 컴파일러에게 더 많은 짐을 안기느냐 아니냐의 차이일 겁니다. 아무리 복잡한 소스 코드도 그 컴파일러 성능을 측정하는 중요한 테스트벤치의 핫 루프 안에 들어 있다면 억지로라도 최적화 합니다. 여러 가지 요인이 개입되지만 "일반적으로는" 컴파일러에게 지시를 더 디테일하게 할수록 컴파일러가 그 이면을 이해하기도 어렵고 최적화를 하기도 상대적으로 더 어렵겠죠.

int가 타겟일 경우와 char가 타겟일 경우 저수준에서 제가 생각할 수 있는 차이는 alignment입니다. int라면 4바이트 얼라인이 되어 있을 테고, char*라면 그렇지 않을 수도 있습니다. 이것도 머신마다 다를 수 있는데, 보통은 align 되지 않은 메모리 영역에 카피하는 명령어와 align 된 영역에 카피하는 명령어 집합이 다르고 성능도 다릅니다. 이조차도 컴파일러가 destination 쪽이 얼라인이 되었는지 아닌지 런타임 체크를 하고, 각각의 경우에 대해 루프를 따로 생성하기도 합니다. 그건 최적화니까 안 할 수도 있고요.

질문으로 되돌아가서 왜 차이가 나느냐면, 저수준의 구현이 다르기 때문이고요. 어떻게 달라지느냐는 일단 생성된 중간 코드나 어셈블리를 봐가면서 분석을 하셔야겠죠.

CAPTCHA의 이미지

void Shift10BitMode(unsigned char* pImage, unsigned char* pDest, unsigned int nWidth, unsigned int nHeight)
{
unsigned int i,j,k;
unsigned int nByteWidth;
unsigned char *piDest;
nByteWidth = nWidth * 5 / 4 ;
piDest = (unsigned char *)pDest;
for(i=0; i {
for(j=0, k=0; j {
// Pointer 연산
//piDest[j]=(pImage[k+3]<<24)+(pImage[k+2]<<16)+(pImage[k+1]<<8)+(pImage[k]);

//memcpy
//memcpy(piDest + z, pImage + k, 4); // 1byte씩 복사를 한다.
//z += 4;

//char 연산
piDest[j] = (pImage[k]);
piDest[j+1] = (pImage[k + 1]);
piDest[j+2] = (pImage[k + 2]);
piDest[j+3] = (pImage[k + 3]);
}
piDest += nWidth;
pImage += nByteWidth;
}

상기 코드의 원문입니다.

답변 감사드립니다, 아래 여러 분들의 답변을 종합하고 이해하면 memecpy의 경우 그 자체 함수가 일단 복잡하고 루틴이 많아서 처리에 속도차이가 난다고 이해하면 될것같네요 또한 알려주신데로 어셈블리를 봐가면서 확인 해봐야겠습니다.

Stephen Kyoungwon Kim@Google의 이미지

piDest[j]=(pImage[k+3]<<24)+(pImage[k+2]<<16)+(pImage[k+1]<<8)+(pImage[k]);

오른쪽은 네 바이트고 왼쪽은 1바이트네요. 제일 낮은 바이트만 들어갈 것 같습니다. piDest[j]의 타입은 unsigned int여야 할 것 같습니다.

답변 감사드립니다, 아래 여러 분들의 답변을 종합하고 이해하면 memecpy의 경우 그 자체 함수가 일단 복잡하고 루틴이 많아서 처리에 속도차이가 난다고 이해하면 될것같네요.

그런 얘기로 보이지 않구요.

memcpy는 N개의 연속된 바이트를 카피하기 위한 루틴입니다. 이걸 구현하는 사람 입장에서 생각해 보세요. 꼭 집어서 4개씩 가져와서 shift와 + 등으로 조립한 다음 4 바이트씩 메모리에 차례로 써나가는 게 반드시 모든 아키텍쳐에서 N개의 연속된 메모리를 카피하는 최선의 방법일까요?극단적인 예로 memcpy가 너무나도 중요한 나머지 하드웨어에서 memcpy R_addr, R_dest, R_size 같은 인스트럭션을 제공, R_addr부터 R_size만큼 R_dest로의 카피를 빠르게 해준다면, 그 인스트럭션을 이용하는 게 낫겠죠. 혹은 하드웨어가 1KB 단위로는 빠르게 카피하는 인스트럭션을 제공한다면, 4바이트씩 조합해서 4바이트씩 쓰는 것보다 그냥 1KB씩 쓰고 나머지에 대해 4바이트씩 쓰는 게 더 나을 테고요.

그래서 연속된 N바이트를 카피하고 그게 충분히 크다면 (루프 안에서 매번 memcpy를 부르는 게 아니라 루프밖에서 한두번만 부르는 식으로 해야겠지만) memcpy를 쓰는 게 낫습니다.

C로 된 소스 코드의 구현은 아마 보통의 인텔 머신에선 안 쓰일 거예요. memcpy는 아키텍쳐마다 어셈블리로 구현된 최적화 버전이 있고, 그걸 사용하지 못할 경우에 소스 코드를 컴파일한, generic한 버전을 사용합니다.

말씀하신 예에서 멤카피가 좋지 않은 건, 큰 N에 대해 연속된 N 바이트를 카피하는 게 아니라 4 바이트씩 반복적으로 카피해야 하기 때문입니다. memcpy는 어셈블리로 구현된 것을 불러오니까 컴파일러가 별도로 최적화하지 않는다면, 매번 함수 호출이 일어납니다. N이 2Mb인 경우 함수 호출 한 번 정도는 아무 것도 아니지만, N이 4 바이트인데 그때마다 호출을 하는 건 부담이죠.

memcpy는 제외하고, memcpy 대비 char to char 카피나 네 개를 쉬프트와 덧셈을 이용해서 조립한 다음 int에 집어넣는 방식 중 어디가 나으냐는 여러 가지 팩터가 개입되기 때문에 일관되게 말하기도 알기도 쉽지는 않다는 게 다른 두 분의 이야기로 이해합니다. 제 얘기도 마찬가지고요.

memcpy만 해도 컴파일러에 따라 충분히 memcpy 호출을 보는데 카피하는 사이즈가 작은 게 확실하고 src와 dst가 오버랩이 되지 않는다면, memcpy를 호출하는 대신 그냥 작은 루프를 생성해서 카피를 수행할 수도 있습니다.

그밖에도 지금 이 예제에서는 piDest가 4 바이트 align이 되어 있어야 하는데, pDest는 그렇지 않습니다. int는 4 바이트고, 대충 말해 4바이트 짜리 오퍼런드를 메모리 상에서 배치할 때는 그 시작 주소가 4의 배수여야 합니다. (alignment라고 하고요)

저도 디테일이 다 기억나진 않습니다. 여러 가지 시나리오가 가능합니다. SPARC 오래된 아키텍쳐의 경우 pDest[j]가 4의 배수가 아니면, 아예 쓰는 게 불가능하고 bus error가 납니다. 프로그램이 죽고요. 4의 배수일 때 사용하는 store 명령밖에 없기 때문에 컴파일러는 그래도 프로그래머를 믿고 그걸 쓰겠죠. 인텔은 misaligned store가 가능합니다. 이 경우에도 아마 오래된 인텔 머신의 경우는 misaligned store보다 aligned store가 빠를 겁니다. 하지만 컴파일러는 pDest[j]가 4의 배수라고 확신할 수 없습니다. 따라서 두 가지 중 하나를 할 텐데, 첫째는 그냥 misaligned store를 사용하는 거구요. 이 경우라면 설령 pDest[j]가 4의 배수, 즉 4바이트 얼라인이 되어 있었어도 최적보다 느리게 카피를 수행하게 되겠죠. 두번째는 컴파일러가 런타임에 pDest[0]이 4의 배수인지 체크합니다. 그래서 그런 경우에 점프하는 루프와 아닌 경우에 점프하는 루프를 따로 만들겠죠. 전자에서는 aligned store를 후자에서는 misaligned store를 사용할 테고, pDest[0]가 4의 배수였다면 성능은 나쁘지 않을 겁니다.

여기에서도 실제 응용 프로그램에서는 컴파일러가 store를 기다리는 동안 수행할 다른 인스트럭션들이 있을 수 있습니다. 혹은 하드웨어가 그런 인스트럭션들을 찾아 채워줄 수도 있고요. 혹은 뭔가 버퍼링을 해두고 나중에 실제로 메모리에 천천히 백그라운드로 카피가 될 수도 있습니다. 이런 경우에는 store의 페널티가 그렇게 드러나지 않을 수도 있겠죠.

요점은 이렇습니다. 소스 코드 상에서 비슷해 보여도 저수준에서는 다르게 나타납니다. 어떻게 달라지느냐, 거기에 영향을 주는 많은 요소들이 있습니다. 컴파일러 최적화나 아키텍쳐도 그 요소의 일부고요. 특히나 컴파일러 최적화는 매우 복잡합니다. 게다가 우리는 지금 메모리 카피가 어느 게 빠른지만 보고 있지만, 실제 응용에서 컴파일러는 메모리 카피 이전에도 코드의 최적화를 많이 수행하기 때문에 컴파일러가 보는 메모리 카피 문제와 우리가 소스 코드의 메모리 카피 문제는 전혀 다를 수도 있습니다.

그래서 만약 정말 여기가 병목이고 여기 성능을 개선해야 한다면, 그래서 어떤 일이 벌어지는지 이해하려면 보통은 중간 코드나 어셈블리를 보게 된다고 생각합니다.

이성일의 이미지

말씀해주신 부분대로, 단순히 코드 부분만을 가지고 논하기에는 부족한점이 많이 있네요,

중간 코드와 어셈블리 부분을 확인 하는 방법을 확인하고 속도를 측정하는 방법을 공부해봐야 겠습니다.

긴 답변 정말 감사드립니다.

라스코니의 이미지

glibc에 있는 memcpy() 함수를 보니 아래와 같습니다.

void *memcpy (void *dstpp, const void *srcpp, size_t len)
{
  unsigned long int dstp = (long int) dstpp;
  unsigned long int srcp = (long int) srcpp;
 
  if (len >= OP_T_THRES)
    {
      len -= (-dstp) % OPSIZ;
      BYTE_COPY_FWD (dstp, srcp, (-dstp) % OPSIZ);
 
      PAGE_COPY_FWD_MAYBE (dstp, srcp, len, len);
 
      WORD_COPY_FWD (dstp, srcp, len, len);
    }
 
  BYTE_COPY_FWD (dstp, srcp, len);
 
  return dstpp;
}

얼핏 봐도 복잡해 보입니다. 보통 유저가 특정 상황에 맞추어 작성한 코드가 제일 빨리 동작합니다. glibc는 보다 일반적인 호환성을 제공하는 것이고요. 11 bit 옮기는 것으로는 성능을 논하기 어려우니 세가지 오퍼레이션 들 중 어떤 것이 빠른지 분석하려면 좀 더 복잡하고 로드를 유발할 수 있는 코드가 필요할 것 같네요.

Stephen Kyoungwon Kim의 이미지

이 코드는 아마 어셈블리 버전의 구현이 사용될 수 없을 때 fallback 되는 generic한 구현일 걸로 보입니다. 제가 glibc 전문가는 아닙니다만, 일반적으로 그렇게 할 거 같고요.

유저가 특정 상황에 맞춰 작성한 코드가 빠르게 동작하는 건, 그 상황이 정말 특수하거나--예컨대 마이크로아키텍쳐를 새로 만들면서 남들은 잘 안 쓰는 Functional Unit과 인스트럭션을 넣어서 컴파일러가 일반적으로는 그 패턴을 매치해서 코드 생성을 하기 어렵다거나--그 유저가 수퍼 유저거나 둘 중에 하나 아닐까 싶습니다. 대부분은 있는 걸 사용하는 게 낫다고 생각합니다.

익명 사용자의 이미지

e.g., sysdeps/i386/memcopy.h

/* memcopy.h -- definitions for memory copy functions.  i386 version.
   Copyright (C) 1991-2016 Free Software Foundation, Inc.
   This file is part of the GNU C Library.
   Contributed by Torbjorn Granlund (tege@sics.se).
 
   The GNU C Library is free software; you can redistribute it and/or
   modify it under the terms of the GNU Lesser General Public
   License as published by the Free Software Foundation; either
   version 2.1 of the License, or (at your option) any later version.
 
   The GNU C Library is distributed in the hope that it will be useful,
   but WITHOUT ANY WARRANTY; without even the implied warranty of
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
   Lesser General Public License for more details.
 
   You should have received a copy of the GNU Lesser General Public
   License along with the GNU C Library; if not, see
   <http://www.gnu.org/licenses/>.  */
 
#include <sysdeps/generic/memcopy.h>
 
#undef	OP_T_THRES
#define	OP_T_THRES	8
 
#undef	BYTE_COPY_FWD
#define BYTE_COPY_FWD(dst_bp, src_bp, nbytes)				      \
  do {									      \
    int __d0;								      \
    asm volatile(/* Clear the direction flag, so copying goes forward.  */    \
		 "cld\n"						      \
		 /* Copy bytes.  */					      \
		 "rep\n"						      \
		 "movsb" :						      \
		 "=D" (dst_bp), "=S" (src_bp), "=c" (__d0) :		      \
		 "0" (dst_bp), "1" (src_bp), "2" (nbytes) :		      \
		 "memory");						      \
  } while (0)
 
#undef	BYTE_COPY_BWD
#define BYTE_COPY_BWD(dst_ep, src_ep, nbytes)				      \
  do									      \
    {									      \
      int __d0;								      \
      asm volatile(/* Set the direction flag, so copying goes backwards.  */  \
		   "std\n"						      \
		   /* Copy bytes.  */					      \
		   "rep\n"						      \
		   "movsb\n"						      \
		   /* Clear the dir flag.  Convention says it should be 0. */ \
		   "cld" :						      \
		   "=D" (dst_ep), "=S" (src_ep), "=c" (__d0) :		      \
		   "0" (dst_ep - 1), "1" (src_ep - 1), "2" (nbytes) :	      \
		   "memory");						      \
      dst_ep += 1;							      \
      src_ep += 1;							      \
    } while (0)
 
#undef	WORD_COPY_FWD
#define WORD_COPY_FWD(dst_bp, src_bp, nbytes_left, nbytes)		      \
  do									      \
    {									      \
      int __d0;								      \
      asm volatile(/* Clear the direction flag, so copying goes forward.  */  \
		   "cld\n"						      \
		   /* Copy longwords.  */				      \
		   "rep\n"						      \
		   "movsl" :						      \
 		   "=D" (dst_bp), "=S" (src_bp), "=c" (__d0) :		      \
		   "0" (dst_bp), "1" (src_bp), "2" ((nbytes) / 4) :	      \
		   "memory");						      \
      (nbytes_left) = (nbytes) % 4;					      \
    } while (0)
 
#undef	WORD_COPY_BWD
#define WORD_COPY_BWD(dst_ep, src_ep, nbytes_left, nbytes)		      \
  do									      \
    {									      \
      int __d0;								      \
      asm volatile(/* Set the direction flag, so copying goes backwards.  */  \
		   "std\n"						      \
		   /* Copy longwords.  */				      \
		   "rep\n"						      \
		   "movsl\n"						      \
		   /* Clear the dir flag.  Convention says it should be 0. */ \
		   "cld" :						      \
		   "=D" (dst_ep), "=S" (src_ep), "=c" (__d0) :		      \
		   "0" (dst_ep - 4), "1" (src_ep - 4), "2" ((nbytes) / 4) :   \
		   "memory");						      \
      dst_ep += 4;							      \
      src_ep += 4;							      \
      (nbytes_left) = (nbytes) % 4;					      \
    } while (0)

jick의 이미지

이런 종류의 optimization은 컴파일러 버전, 컴파일러 옵션, 사용한 변수의 정확한 타입, 그 외에 수많은 자잘한 이유로 다른 결과가 나올 수 있기 때문에, 너무 큰 의미를 두지 않는 것이 좋습니다.

예를 들면 이런 걸 보죠:

https://godbolt.org/z/eEIGK7

int f1(int *p)
{
    char *q = (char *) p;
    return q[0] + (q[1] << 8) + (q[2] << 16) + (q[3] << 24);
}
 
int f2(unsigned int *p)
{
    unsigned char *q = (unsigned char *) p;
    return q[0] + (q[1] << 8) + (q[2] << 16) + (q[3] << 24);
}

결과 (clang 9.0.0):

f1(int*):                                # @f1(int*)
        movsx   eax, byte ptr [rdi]
        movsx   ecx, byte ptr [rdi + 1]
        shl     ecx, 8
        add     ecx, eax
        movsx   edx, byte ptr [rdi + 2]
        shl     edx, 16
        add     edx, ecx
        movzx   eax, byte ptr [rdi + 3]
        shl     eax, 24
        add     eax, edx
        ret
f2(unsigned int*):                                # @f2(unsigned int*)
        mov     eax, dword ptr [rdi]
        ret

* godbolt는 진리입니다.

사족의 이미지

f1은 주어진 int값을 그대로 반환하는 함수가 아닙니다.
아마 의도적으로 그렇게 작성하신 것 같네요. :)

경험 있는 프로그래머가 실수로 저런 코드를 작성했을 경우, 어셈블된 결과에서 movsx를 보자마자 실수를 깨달을 수 있겠지요.

만약 그렇지 않다면, 부연설명이 조금 더 필요하겠습니다. 이런 건 어때요?

int f1(int *p)
{
    char *q = (char *) p;
    return q[0] + (q[1] << 8) + (q[2] << 16) + (q[3] << 24);
}
 
int f2(unsigned int *p)
{
    unsigned char *q = (unsigned char *) p;
    return q[0] + (q[1] << 8) + (q[2] << 16) + (q[3] << 24);
}
 
template <typename T>
void print(T val);
 
void test(){
    int p = 255;
    unsigned int up = 255;
    print(f1(&p)); // f1과 f2에 둘 다 255를 주면...
    print(f2(&up));
}

f1(int*):                                # @f1(int*)
        movsx   eax, byte ptr [rdi]
        movsx   ecx, byte ptr [rdi + 1]
        shl     ecx, 8
        add     ecx, eax
        movsx   edx, byte ptr [rdi + 2]
        shl     edx, 16
        add     edx, ecx
        movzx   eax, byte ptr [rdi + 3]
        shl     eax, 24
        add     eax, edx
        ret
f2(unsigned int*):                                # @f2(unsigned int*)
        mov     eax, dword ptr [rdi]
        ret
test():                               # @test()
        push    rax
        mov     edi, -1 # f1의 반환값은 -1 (0xffffffff)
        call    void print<int>(int)
        mov     edi, 255 # f2의 반환값은 255
        pop     rax
        jmp     void print<int>(int)          # TAILCALL

사실 f1f2의 핵심적인 차이는, f1char를 쓰고 있다는 겁니다.
그리고 char이 부호가 있는지 없는지는 문자 그대로 implementation에 따라 다릅니다.

댓글 달기

Filtered HTML

  • 텍스트에 BBCode 태그를 사용할 수 있습니다. URL은 자동으로 링크 됩니다.
  • 사용할 수 있는 HTML 태그: <p><div><span><br><a><em><strong><del><ins><b><i><u><s><pre><code><cite><blockquote><ul><ol><li><dl><dt><dd><table><tr><td><th><thead><tbody><h1><h2><h3><h4><h5><h6><img><embed><object><param><hr>
  • 다음 태그를 이용하여 소스 코드 구문 강조를 할 수 있습니다: <code>, <blockcode>, <apache>, <applescript>, <autoconf>, <awk>, <bash>, <c>, <cpp>, <css>, <diff>, <drupal5>, <drupal6>, <gdb>, <html>, <html5>, <java>, <javascript>, <ldif>, <lua>, <make>, <mysql>, <perl>, <perl6>, <php>, <pgsql>, <proftpd>, <python>, <reg>, <spec>, <ruby>. 지원하는 태그 형식: <foo>, [foo].
  • web 주소와/이메일 주소를 클릭할 수 있는 링크로 자동으로 바꿉니다.

BBCode

  • 텍스트에 BBCode 태그를 사용할 수 있습니다. URL은 자동으로 링크 됩니다.
  • 다음 태그를 이용하여 소스 코드 구문 강조를 할 수 있습니다: <code>, <blockcode>, <apache>, <applescript>, <autoconf>, <awk>, <bash>, <c>, <cpp>, <css>, <diff>, <drupal5>, <drupal6>, <gdb>, <html>, <html5>, <java>, <javascript>, <ldif>, <lua>, <make>, <mysql>, <perl>, <perl6>, <php>, <pgsql>, <proftpd>, <python>, <reg>, <spec>, <ruby>. 지원하는 태그 형식: <foo>, [foo].
  • 사용할 수 있는 HTML 태그: <p><div><span><br><a><em><strong><del><ins><b><i><u><s><pre><code><cite><blockquote><ul><ol><li><dl><dt><dd><table><tr><td><th><thead><tbody><h1><h2><h3><h4><h5><h6><img><embed><object><param>
  • web 주소와/이메일 주소를 클릭할 수 있는 링크로 자동으로 바꿉니다.

Textile

  • 다음 태그를 이용하여 소스 코드 구문 강조를 할 수 있습니다: <code>, <blockcode>, <apache>, <applescript>, <autoconf>, <awk>, <bash>, <c>, <cpp>, <css>, <diff>, <drupal5>, <drupal6>, <gdb>, <html>, <html5>, <java>, <javascript>, <ldif>, <lua>, <make>, <mysql>, <perl>, <perl6>, <php>, <pgsql>, <proftpd>, <python>, <reg>, <spec>, <ruby>. 지원하는 태그 형식: <foo>, [foo].
  • You can use Textile markup to format text.
  • 사용할 수 있는 HTML 태그: <p><div><span><br><a><em><strong><del><ins><b><i><u><s><pre><code><cite><blockquote><ul><ol><li><dl><dt><dd><table><tr><td><th><thead><tbody><h1><h2><h3><h4><h5><h6><img><embed><object><param><hr>

Markdown

  • 다음 태그를 이용하여 소스 코드 구문 강조를 할 수 있습니다: <code>, <blockcode>, <apache>, <applescript>, <autoconf>, <awk>, <bash>, <c>, <cpp>, <css>, <diff>, <drupal5>, <drupal6>, <gdb>, <html>, <html5>, <java>, <javascript>, <ldif>, <lua>, <make>, <mysql>, <perl>, <perl6>, <php>, <pgsql>, <proftpd>, <python>, <reg>, <spec>, <ruby>. 지원하는 태그 형식: <foo>, [foo].
  • Quick Tips:
    • Two or more spaces at a line's end = Line break
    • Double returns = Paragraph
    • *Single asterisks* or _single underscores_ = Emphasis
    • **Double** or __double__ = Strong
    • This is [a link](http://the.link.example.com "The optional title text")
    For complete details on the Markdown syntax, see the Markdown documentation and Markdown Extra documentation for tables, footnotes, and more.
  • web 주소와/이메일 주소를 클릭할 수 있는 링크로 자동으로 바꿉니다.
  • 사용할 수 있는 HTML 태그: <p><div><span><br><a><em><strong><del><ins><b><i><u><s><pre><code><cite><blockquote><ul><ol><li><dl><dt><dd><table><tr><td><th><thead><tbody><h1><h2><h3><h4><h5><h6><img><embed><object><param><hr>

Plain text

  • HTML 태그를 사용할 수 없습니다.
  • web 주소와/이메일 주소를 클릭할 수 있는 링크로 자동으로 바꿉니다.
  • 줄과 단락은 자동으로 분리됩니다.
댓글 첨부 파일
이 댓글에 이미지나 파일을 업로드 합니다.
파일 크기는 8 MB보다 작아야 합니다.
허용할 파일 형식: txt pdf doc xls gif jpg jpeg mp3 png rar zip.
CAPTCHA
이것은 자동으로 스팸을 올리는 것을 막기 위해서 제공됩니다.