Broadly, the answer is "it depends." The way one would send particle updates is quite a bit different than the way you would send a series of GPU-skinned models.
In the general sense, though, everything these days is done with a VBO (vertex buffer object). The old immediate-mode API (
glEnd) is probably just implemented as a fat wrapper around the driver's internal vertex buffer system.
For static objects, create a static VBO and fill it with your vertex data. If any of the vertices are shared (usually the case), you probably also want to create an index buffer. That both reduces the need to send the same data more than once for changing meshes (potentially saving on transfer bandwidth) and on processing (saving on vertex shading time). Note that you draw with different functions when making indexed vs non-indexed draws.
For dynamic objects, do the same, albeit with a dynamic set of buffers.
For larger pieces like terrain, you probably won't to break up the mesh into multiple pieces. Making the GPU render a hundred million triangles when only two hundred thousand of them are visible is a huge waste, especially if they're unsorted and there's lots of overdraw and wasted fragment shader invocations. Break up the mesh into large chunks and then only render those that are within the view frustrum. There's also various more advanced culling techniques you can use to weed out chunks that might be in the frustrum but are entirely behind a hill or building or something. Keeping draw call count down is good, but there's a balance to be had (which you have to find for your specific app/hardware) between minimizing draw calls and minimizing the drawing of hidden geometry.
One of the key things to keep in mind with a GPU buffer is that you cannot write to it while the GPU is reading from it. You need to let the driver know that it's okay to discard the old copy of the buffer (when it's done) and to give you a new one (if the old one is busy). There is of course for a long time was no function is OpenGL to do this (now there is InvalidateBufferData for GL 4.3 and some older implementations as an extension). Rather, there is a non-standard but common behavior most drivers implement. Do this to discard the buffer before updating it:
glBufferData(GL_ARRAY_BUFFER, 0, NULL, GL_DYNAMIC_DRAW);
Of course change
GL_DYNAMIC_DRAW to the appropriate values for the buffer. Static buffers won't be updated (or shouldn't be) so you're unlikely to need to worry about discard for such a buffer.
Note that it might be faster to use
glBufferSubData or it might be faster with
glMapBuffer. It really depends on the driver and hardware. Current generation PC hardware will probably be fastest with
glBufferData but test to be sure.
Another technique is to use instancing. Instancing allows you to make a single draw call that draws multiple copies of the data in a vertex/index buffer. If you had, say, 100 identical rocks then you would want to draw them all in one go rather than making 100 independent draws.
When instancing you need to put per-instance data in another buffer (like each individual's object position). This can either be a uniform buffer (constant buffer in D3D terminology) or a texture buffer or a per-instance vertex attribute. Again, which is faster depends. Per-instance attributes are probably faster and definitely way easier to use, but many common GL implementations still don't support
glBindingAttribDivisor so you'll have to see if it's available to use and if it really is faster (some older drivers emulated instancing by appending buffers and it ended up being slower to use instancing on them than to make independent draw calls and there's no standard way to find out... the joys of using OpenGL).
There are also algorithms for vertex cache optimization, which is the act of ordering vertices/indices in your buffers to play will with the vertex cache on modern GPUs. A GPU will only run the shader for a vertex then cache it in the vertex cache but it might have to be evicted too early to make room for other vertices. (Say, two triangles both share a vertex but there are 100 other triangles drawn between them; the shared vertex will probably end up being wastefully evaluated by the vertex shader twice.)
Some of these features require a new-enough version of GL or GLES. GLES2 did not support instancing, for example.
Again, if you care about performance, test each possible method and see which is faster for your app on your target hardware. Not only will different hardware/drivers from different manufacturers be different but some whole classes of hardware are innately different. A typical mobile GPU is a very different beast from a typical discrete desktop GPU. Techniques that are "best" on one will not necessarily be best on another.
When it comes to performance, always be a skeptic.